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.
5const 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
45export function parsePiDiscoveredCommands(data: unknown): PiDiscoveredCommands {
46 const commands = recordField(data, "commands");
47 if (!Array.isArray(commands)) return { slashCommands: [], skills: [] };
48 const slashCommands: Array<ServerProviderSlashCommand> = [];
49 const skills: Array<ServerProviderSkill> = [];
50 for (const command of commands) {
51 const commandName = recordString(command, "name");
52 if (commandName === undefined || commandName.length === 0) continue;
53 const description = recordString(command, "description");
54 if (recordString(command, "source") === "skill") {
55 const name = commandName.startsWith("skill:")
56 ? commandName.slice("skill:".length)
57 : commandName;
58 if (name.length === 0) continue;
59 const sourceInfo = recordField(command, "sourceInfo");
60 const commandInterface = recordField(command, "interface");
61 const path =
62 recordString(sourceInfo, "path") ?? recordString(command, "path") ?? `pi:skill:${name}`;
63 const scope = normalizePiSkillScope(
64 recordString(sourceInfo, "scope") ?? recordString(command, "location"),
65 );
66 const displayName =
67 recordString(command, "displayName") ??
68 recordString(sourceInfo, "displayName") ??
69 recordString(commandInterface, "displayName");
70 const shortDescription =
71 recordString(command, "shortDescription") ??
72 recordString(sourceInfo, "shortDescription") ??
73 recordString(commandInterface, "shortDescription");
74 skills.push({
75 name,
76 path,
77 enabled: true,
78 ...(description === undefined ? {} : { description }),
79 ...(scope === undefined ? {} : { scope }),
80 ...(displayName === undefined ? {} : { displayName }),
81 ...(shortDescription === undefined ? {} : { shortDescription }),
82 });
83 continue;
84 }
85 slashCommands.push({
86 name: commandName,
87 ...(description === undefined ? {} : { description }),
88 });
89 }
90 return { slashCommands, skills };
91}
92
93function recordField(input: unknown, key: string): unknown {
94 return Predicate.isObject(input) ? input[key] : undefined;
95}
96
97function recordString(input: unknown, key: string): string | undefined {
98 const value = recordField(input, key);
99 return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
100}