1import { type ServerProviderSkill, type ServerProviderSlashCommand } from "@t3tools/contracts";
2import * as Predicate from "effect/Predicate";
3
4// Pi RPC get_commands omits TUI builtins. Advertise /compact so T3 can map it to RPC compact.
5export const PI_COMPACT_SLASH_COMMAND: ServerProviderSlashCommand = {
6 name: "compact",
7 description: "Summarize the conversation and reduce context usage",
8 input: { hint: "Optional instructions" },
9};
10
11export interface PiCompactCommand {
12 readonly customInstructions?: string;
13}
14
15export function parsePiCompactCommand(text: string): PiCompactCommand | null {
16 const trimmed = text.trim();
17 if (trimmed === "/compact") return {};
18 if (!trimmed.startsWith("/compact")) return null;
19 const rest = trimmed.slice("/compact".length);
20 if (rest.length === 0) return {};
21 if (!/^\s/.test(rest)) return null;
22 const customInstructions = rest.trim();
23 return customInstructions.length === 0 ? {} : { customInstructions };
24}
25
26export function withPiBuiltinSlashCommands(
27 commands: ReadonlyArray<ServerProviderSlashCommand>,
28): ReadonlyArray<ServerProviderSlashCommand> {
29 return [PI_COMPACT_SLASH_COMMAND, ...commands.filter((command) => command.name !== "compact")];
30}
31
32export interface PiDiscoveredCommands {
33 readonly slashCommands: ReadonlyArray<ServerProviderSlashCommand>;
34 readonly skills: ReadonlyArray<ServerProviderSkill>;
35}
36
37function normalizePiSkillScope(scope: string | undefined): string | undefined {
38 if (scope === undefined) return undefined;
39 const normalized = scope.trim().toLowerCase();
40 if (normalized === "global" || normalized === "personal") return "user";
41 if (normalized === "workspace" || normalized === "local") return "project";
42 return scope;
43}
44
45/** Maps Pi's `get_commands` payload to T3's shared command and skill surfaces. */
46export function parsePiDiscoveredCommands(data: unknown): PiDiscoveredCommands {
47 const commands = recordField(data, "commands");
48 if (!Array.isArray(commands)) return { slashCommands: [], skills: [] };
49 const slashCommands: Array<ServerProviderSlashCommand> = [];
50 const skills: Array<ServerProviderSkill> = [];
51 for (const command of commands) {
52 const commandName = recordString(command, "name");
53 if (commandName === undefined || commandName.length === 0) continue;
54 const description = recordString(command, "description");
55 if (recordString(command, "source") === "skill") {
56 const name = commandName.startsWith("skill:")
57 ? commandName.slice("skill:".length)
58 : commandName;
59 if (name.length === 0) continue;
60 const sourceInfo = recordField(command, "sourceInfo");
61 const commandInterface = recordField(command, "interface");
62 const path =
63 recordString(sourceInfo, "path") ?? recordString(command, "path") ?? `pi:skill:${name}`;
64 const scope = normalizePiSkillScope(
65 recordString(sourceInfo, "scope") ?? recordString(command, "location"),
66 );
67 const displayName =
68 recordString(command, "displayName") ??
69 recordString(sourceInfo, "displayName") ??
70 recordString(commandInterface, "displayName");
71 const shortDescription =
72 recordString(command, "shortDescription") ??
73 recordString(sourceInfo, "shortDescription") ??
74 recordString(commandInterface, "shortDescription");
75 skills.push({
76 name,
77 path,
78 enabled: true,
79 ...(description === undefined ? {} : { description }),
80 ...(scope === undefined ? {} : { scope }),
81 ...(displayName === undefined ? {} : { displayName }),
82 ...(shortDescription === undefined ? {} : { shortDescription }),
83 });
84 continue;
85 }
86 slashCommands.push({
87 name: commandName,
88 ...(description === undefined ? {} : { description }),
89 });
90 }
91 return { slashCommands, skills };
92}
93
94function recordField(input: unknown, key: string): unknown {
95 return Predicate.isObject(input) ? input[key] : undefined;
96}
97
98function recordString(input: unknown, key: string): string | undefined {
99 const value = recordField(input, key);
100 return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
101}