@pandi-coding-agent/pandi-kitty 0.1.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/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # pandi-kitty
2
+
3
+ Extensión de Pi para controlar el terminal [kitty](https://sw.kovidgoyal.net/kitty/)
4
+ en ejecución vía su protocolo de control remoto (`kitty @ ...`).
5
+
6
+ ## Requisitos
7
+
8
+ `allow_remote_control yes` en `kitty.conf` (o `kitty -o allow_remote_control=yes`), y
9
+ correr Pi desde una sesión de kitty en ejecución.
10
+
11
+ ## Comando
12
+
13
+ ```
14
+ /kitty tab nueva tab
15
+ /kitty window nueva ventana (según el layout activo)
16
+ /kitty vsplit nueva ventana en split vertical
17
+ /kitty hsplit nueva ventana en split horizontal
18
+ /kitty os-window nueva ventana de OS
19
+ /kitty layout <nombre> cambia el layout activo (ej. splits, tall, fat, grid)
20
+ /kitty close [id] cierra una ventana (la activa si se omite el id)
21
+ /kitty focus <id> enfoca una ventana por id
22
+ ```
23
+
24
+ ## Tool
25
+
26
+ `kitty_remote` — acciones `launch`, `goto-layout`, `close-window`, `focus-window`.
27
+ Ver `index.ts` para el esquema de parámetros completo.
28
+
29
+ ## Nota
30
+
31
+ `--location vsplit/hsplit` solo tiene efecto visible bajo el layout `splits`. Bajo
32
+ otros layouts (`tall`, `fat`, `grid`, ...) la ventana se abre igual, pero la
33
+ dirección del split la decide ese layout.
package/command.ts ADDED
@@ -0,0 +1,6 @@
1
+ /** Parseo puro de `/kitty <subcomando> [args...]` en acción + tokens. */
2
+ export function parseKittyCommand(input: string): { action: string; rest: string[] } {
3
+ const tokens = (input ?? "").trim().split(/\s+/).filter(Boolean);
4
+ const action = (tokens.shift() ?? "tab").toLowerCase();
5
+ return { action, rest: tokens };
6
+ }
package/index.ts ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * pandi-kitty: controla el terminal kitty en ejecución desde Pi vía su protocolo
3
+ * de control remoto (`kitty @ ...`).
4
+ *
5
+ * Dos superficies (convención del proyecto; ver pandi-worktree):
6
+ * - `/kitty` comando slash para personas (tab | window | vsplit | hsplit | layout <nombre>)
7
+ * - `kitty_remote` tool invocable por el modelo (acciones explícitas)
8
+ *
9
+ * Ambas comparten los manejadores puros de `./kitty.ts`. `kitty` siempre se invoca con
10
+ * un array ARGV (nunca un string de shell).
11
+ *
12
+ * Requiere `allow_remote_control yes` en kitty.conf (o `-o allow_remote_control=yes` al
13
+ * arrancar kitty) y correr DESDE una sesión de kitty en ejecución.
14
+ */
15
+
16
+ import { StringEnum } from "@earendil-works/pi-ai";
17
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
18
+ import { Type } from "typebox";
19
+ import { parseKittyCommand } from "./command.js";
20
+ import {
21
+ DEFAULT_KITTY_TIMEOUT_MS,
22
+ type HandlerResult,
23
+ parseTimeoutMs,
24
+ runCloseWindow,
25
+ runFocusWindow,
26
+ runGotoLayout,
27
+ runKitty,
28
+ runLaunch,
29
+ SPLIT_LOCATIONS,
30
+ WINDOW_TYPES,
31
+ } from "./kitty.js";
32
+ import { notify } from "./notify.js";
33
+
34
+ export { parseKittyCommand } from "./command.js";
35
+ export {
36
+ buildCloseWindowArgs,
37
+ buildFocusWindowArgs,
38
+ buildGotoLayoutArgs,
39
+ buildLaunchArgs,
40
+ describeError,
41
+ runCloseWindow,
42
+ runFocusWindow,
43
+ runGotoLayout,
44
+ runKitty,
45
+ runLaunch,
46
+ SPLIT_LOCATIONS,
47
+ WINDOW_TYPES,
48
+ } from "./kitty.js";
49
+
50
+ const HELP_TEXT = [
51
+ "Uso:",
52
+ " /kitty tab new tab",
53
+ " /kitty window nueva ventana (según el layout activo)",
54
+ " /kitty vsplit nueva ventana en split vertical",
55
+ " /kitty hsplit nueva ventana en split horizontal",
56
+ " /kitty os-window nueva ventana de OS",
57
+ " /kitty layout <nombre> cambia el layout activo (ej. splits, tall, fat, grid)",
58
+ " /kitty close [id] cierra una ventana (la activa si se omite el id)",
59
+ " /kitty focus <id> enfoca una ventana por id",
60
+ "",
61
+ "Requiere `allow_remote_control yes` en kitty.conf y correr desde una sesión de kitty.",
62
+ ].join("\n");
63
+
64
+ function buildOpts(cwd: string, signal: AbortSignal | null | undefined) {
65
+ return {
66
+ cwd,
67
+ signal: signal ?? undefined,
68
+ timeoutMs: parseTimeoutMs(process.env.PI_KITTY_TIMEOUT_MS, DEFAULT_KITTY_TIMEOUT_MS),
69
+ };
70
+ }
71
+
72
+ // --------------------------------------------------------------------------
73
+ // Handler del comando
74
+ // --------------------------------------------------------------------------
75
+
76
+ async function runCommand(ctx: ExtensionContext, input: string): Promise<void> {
77
+ const { action, rest } = parseKittyCommand(input);
78
+ const opts = buildOpts(ctx.cwd, ctx.signal);
79
+
80
+ if (action === "help" || action === "-h" || action === "--help") {
81
+ notify(ctx, HELP_TEXT, "info");
82
+ return;
83
+ }
84
+
85
+ let result: HandlerResult;
86
+ switch (action) {
87
+ case "tab":
88
+ result = await runLaunch(runKitty, { type: "tab" }, opts);
89
+ break;
90
+ case "window":
91
+ result = await runLaunch(runKitty, { type: "window" }, opts);
92
+ break;
93
+ case "vsplit":
94
+ result = await runLaunch(runKitty, { type: "window", location: "vsplit" }, opts);
95
+ break;
96
+ case "hsplit":
97
+ result = await runLaunch(runKitty, { type: "window", location: "hsplit" }, opts);
98
+ break;
99
+ case "os-window":
100
+ result = await runLaunch(runKitty, { type: "os-window" }, opts);
101
+ break;
102
+ case "layout":
103
+ result = await runGotoLayout(runKitty, { layout: rest[0] ?? "" }, opts);
104
+ break;
105
+ case "close":
106
+ result = await runCloseWindow(runKitty, { matchId: rest[0] }, opts);
107
+ break;
108
+ case "focus":
109
+ result = await runFocusWindow(runKitty, { matchId: rest[0] ?? "" }, opts);
110
+ break;
111
+ default:
112
+ notify(ctx, `Subcomando desconocido: ${action}\n\n${HELP_TEXT}`, "warning");
113
+ return;
114
+ }
115
+ notify(ctx, result.text, result.ok ? "info" : "error");
116
+ }
117
+
118
+ // --------------------------------------------------------------------------
119
+ // Adaptador del resultado de la tool
120
+ // --------------------------------------------------------------------------
121
+
122
+ function toToolResult(result: HandlerResult) {
123
+ return { content: [{ type: "text" as const, text: result.text }], details: result.details };
124
+ }
125
+
126
+ // --------------------------------------------------------------------------
127
+ // Entrada de la extensión
128
+ // --------------------------------------------------------------------------
129
+
130
+ export default function kittyExtension(pi: ExtensionAPI): void {
131
+ pi.registerCommand("kitty", {
132
+ description:
133
+ "Controlá kitty vía remote control: tab | window | vsplit | hsplit | os-window | layout | close | focus",
134
+ handler: async (args, ctx) => {
135
+ await runCommand(ctx, args);
136
+ },
137
+ });
138
+
139
+ pi.registerTool({
140
+ name: "kitty_remote",
141
+ label: "Control remoto de kitty",
142
+ description:
143
+ "Controlá el terminal kitty en ejecución vía su protocolo de control remoto (`kitty @ ...`). Acciones: 'launch' (abre tab/window/os-window nueva, opcionalmente con split vsplit/hsplit), 'goto-layout' (cambia el layout activo), 'close-window' (cierra una ventana), 'focus-window' (enfoca una ventana por id). Requiere `allow_remote_control yes` en kitty.conf y correr desde una sesión de kitty en ejecución.",
144
+ promptSnippet: "Controlá kitty (tabs, ventanas, splits, layout) vía su protocolo de control remoto.",
145
+ promptGuidelines: [
146
+ "Usá kitty_remote para abrir tabs/ventanas/splits de kitty o cambiar su layout en lugar de instruir al usuario a hacerlo manualmente.",
147
+ "Para 'launch' con type:'window', pasá location:'vsplit' o 'hsplit' para un split; el split solo se ve como tal si el layout activo es 'splits' (usá goto-layout primero si hace falta).",
148
+ "Si kitty_remote falla con un error de socket, es porque no hay remote control habilitado (allow_remote_control yes en kitty.conf) o no se está corriendo desde dentro de una sesión de kitty — no reintentes a ciegas, avisale al usuario.",
149
+ ],
150
+ parameters: Type.Object({
151
+ action: StringEnum(["launch", "goto-layout", "close-window", "focus-window"] as const),
152
+ type: Type.Optional(StringEnum(WINDOW_TYPES, { description: "Para launch: tab | os-window | window." })),
153
+ location: Type.Optional(
154
+ StringEnum(SPLIT_LOCATIONS, { description: "Para launch con type:'window': vsplit | hsplit." }),
155
+ ),
156
+ layout: Type.Optional(
157
+ Type.String({ description: "Para goto-layout: nombre del layout (ej. splits, tall, fat, grid)." }),
158
+ ),
159
+ matchId: Type.Optional(
160
+ Type.String({ description: "Para close-window/focus-window: id de la ventana objetivo." }),
161
+ ),
162
+ }),
163
+ executionMode: "sequential",
164
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
165
+ const opts = buildOpts(ctx.cwd, signal);
166
+ switch (params.action) {
167
+ case "launch":
168
+ return toToolResult(
169
+ await runLaunch(runKitty, { type: params.type ?? "tab", location: params.location }, opts),
170
+ );
171
+ case "goto-layout":
172
+ return toToolResult(await runGotoLayout(runKitty, { layout: params.layout ?? "" }, opts));
173
+ case "close-window":
174
+ return toToolResult(await runCloseWindow(runKitty, { matchId: params.matchId }, opts));
175
+ case "focus-window":
176
+ return toToolResult(await runFocusWindow(runKitty, { matchId: params.matchId ?? "" }, opts));
177
+ default:
178
+ return {
179
+ content: [{ type: "text" as const, text: `Acción desconocida: ${params.action}` }],
180
+ details: { isError: true },
181
+ };
182
+ }
183
+ },
184
+ });
185
+ }
package/kitty.ts ADDED
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Utilidades puras + un único punto de ejecución para la extensión pandi-kitty.
3
+ *
4
+ * kitty expone su protocolo de control remoto vía `kitty @ <subcomando>`. Solo funciona
5
+ * desde una sesión de kitty en ejecución con `allow_remote_control` habilitado, y habla
6
+ * con esa instancia por un socket local.
7
+ * - `runKitty` lanza `kitty` con un array ARGV (nunca un string de shell).
8
+ * - `build*Args` son constructores puros de argv (testeados exactamente).
9
+ * - los manejadores `run*` reciben una fn `run` inyectada para que el despacho sea
10
+ * determinista en tests sin necesitar una sesión de kitty real.
11
+ */
12
+
13
+ import { spawn } from "node:child_process";
14
+
15
+ // --------------------------------------------------------------------------
16
+ // Punto de spawn
17
+ // --------------------------------------------------------------------------
18
+
19
+ export interface KittyResult {
20
+ ok: boolean;
21
+ stdout: string;
22
+ stderr: string;
23
+ exitCode?: number;
24
+ timedOut?: boolean;
25
+ /** Se setea cuando nunca logramos hacer spawn de `kitty` (ej. no está instalado). */
26
+ spawnError?: string;
27
+ }
28
+
29
+ export interface RunKittyOptions {
30
+ cwd?: string;
31
+ signal?: AbortSignal;
32
+ timeoutMs?: number;
33
+ /** Binario a ejecutar; redefinible para que los tests apunten a un nombre garantizadamente ausente. */
34
+ bin?: string;
35
+ }
36
+
37
+ export const DEFAULT_KITTY_TIMEOUT_MS = 15_000;
38
+ export const MIN_KITTY_TIMEOUT_MS = 1_000;
39
+
40
+ export function parseTimeoutMs(raw: string | undefined, fallback = DEFAULT_KITTY_TIMEOUT_MS): number {
41
+ const n = Number(raw);
42
+ if (!Number.isFinite(n) || n <= 0) return fallback;
43
+ return Math.max(MIN_KITTY_TIMEOUT_MS, Math.floor(n));
44
+ }
45
+
46
+ /** Firma compartida por runKitty y el runner simulado inyectado en tests. */
47
+ export type RunKitty = (args: string[], options?: RunKittyOptions) => Promise<KittyResult>;
48
+
49
+ /**
50
+ * Hace spawn de `kitty @ ...` con un array argv. Falla de spawn, exit no cero, timeout o
51
+ * abort vuelven como un KittyResult (nunca lanza).
52
+ */
53
+ export function runKitty(args: string[], options: RunKittyOptions = {}): Promise<KittyResult> {
54
+ const { cwd, signal, timeoutMs = DEFAULT_KITTY_TIMEOUT_MS, bin = "kitty" } = options;
55
+ return new Promise((resolve) => {
56
+ let stdout = "";
57
+ let stderr = "";
58
+ let settled = false;
59
+ let timedOut = false;
60
+
61
+ const child = spawn(bin, ["@", ...args], { cwd, windowsHide: true });
62
+
63
+ const finish = (result: KittyResult) => {
64
+ if (settled) return;
65
+ settled = true;
66
+ clearTimeout(timer);
67
+ if (signal) signal.removeEventListener("abort", onAbort);
68
+ resolve(result);
69
+ };
70
+
71
+ const onAbort = () => {
72
+ timedOut = true;
73
+ child.kill("SIGTERM");
74
+ };
75
+ if (signal) {
76
+ if (signal.aborted) onAbort();
77
+ else signal.addEventListener("abort", onAbort, { once: true });
78
+ }
79
+
80
+ const timer = setTimeout(() => {
81
+ timedOut = true;
82
+ child.kill("SIGTERM");
83
+ }, timeoutMs);
84
+
85
+ child.stdout?.on("data", (chunk) => {
86
+ stdout += chunk.toString();
87
+ });
88
+ child.stderr?.on("data", (chunk) => {
89
+ stderr += chunk.toString();
90
+ });
91
+ child.on("error", (err: Error) => {
92
+ finish({ ok: false, stdout, stderr, spawnError: err.message });
93
+ });
94
+ child.on("close", (code) => {
95
+ finish({ ok: !timedOut && code === 0, stdout, stderr, exitCode: code ?? undefined, timedOut });
96
+ });
97
+ });
98
+ }
99
+
100
+ // --------------------------------------------------------------------------
101
+ // Constructores de argv (puros)
102
+ // --------------------------------------------------------------------------
103
+
104
+ export const WINDOW_TYPES = ["tab", "os-window", "window"] as const;
105
+ export type WindowType = (typeof WINDOW_TYPES)[number];
106
+
107
+ export const SPLIT_LOCATIONS = ["vsplit", "hsplit"] as const;
108
+ export type SplitLocation = (typeof SPLIT_LOCATIONS)[number];
109
+
110
+ export interface LaunchOptions {
111
+ type: WindowType;
112
+ location?: SplitLocation;
113
+ cwd?: string;
114
+ }
115
+
116
+ export function buildLaunchArgs(opts: LaunchOptions): string[] {
117
+ const args = ["launch", "--type", opts.type];
118
+ if (opts.location) args.push("--location", opts.location);
119
+ if (opts.cwd) args.push("--cwd", opts.cwd);
120
+ return args;
121
+ }
122
+
123
+ export function buildGotoLayoutArgs(layout: string): string[] {
124
+ return ["goto-layout", layout];
125
+ }
126
+
127
+ export function buildCloseWindowArgs(opts: { matchId?: string } = {}): string[] {
128
+ const args = ["close-window"];
129
+ if (opts.matchId) args.push("--match", `id:${opts.matchId}`);
130
+ return args;
131
+ }
132
+
133
+ export function buildFocusWindowArgs(matchId: string): string[] {
134
+ return ["focus-window", "--match", `id:${matchId}`];
135
+ }
136
+
137
+ // --------------------------------------------------------------------------
138
+ // Normalización de errores
139
+ // --------------------------------------------------------------------------
140
+
141
+ const NOT_RUNNING_HINT =
142
+ "No se pudo hablar con kitty por el socket de control remoto. Verificá que estés dentro de una sesión de kitty y que `allow_remote_control yes` esté en kitty.conf (o iniciá kitty con -o allow_remote_control=yes).";
143
+
144
+ /** Convierte un KittyResult fallido en una sola línea acotada y accionable. */
145
+ export function describeError(result: KittyResult, action: string): string {
146
+ if (result.spawnError) {
147
+ if (/ENOENT/i.test(result.spawnError)) return "No se encontró el binario `kitty` en el PATH.";
148
+ return `No se pudo ejecutar \`kitty @ ${action}\`: ${result.spawnError}`;
149
+ }
150
+ if (result.timedOut) return `\`kitty @ ${action}\` agotó el tiempo de espera.`;
151
+ const detail = (result.stderr || result.stdout || "").trim();
152
+ if (/no such remote control socket|could not connect|is kitty running/i.test(detail)) return NOT_RUNNING_HINT;
153
+ return detail
154
+ ? `\`kitty @ ${action}\` falló: ${detail}`
155
+ : `\`kitty @ ${action}\` falló (salida ${result.exitCode ?? "?"}).`;
156
+ }
157
+
158
+ // --------------------------------------------------------------------------
159
+ // Manejadores de alto nivel (reciben un runner inyectado; por lo demás, despacho puro)
160
+ // --------------------------------------------------------------------------
161
+
162
+ export interface HandlerResult {
163
+ ok: boolean;
164
+ text: string;
165
+ details: Record<string, unknown>;
166
+ }
167
+
168
+ export interface HandlerOpts {
169
+ cwd?: string;
170
+ signal?: AbortSignal;
171
+ timeoutMs?: number;
172
+ }
173
+
174
+ function isWindowType(type: string): type is WindowType {
175
+ return (WINDOW_TYPES as readonly string[]).includes(type);
176
+ }
177
+
178
+ function isSplitLocation(location: string): location is SplitLocation {
179
+ return (SPLIT_LOCATIONS as readonly string[]).includes(location);
180
+ }
181
+
182
+ export async function runLaunch(
183
+ run: RunKitty,
184
+ params: { type: string; location?: string; cwd?: string },
185
+ opts: HandlerOpts,
186
+ ): Promise<HandlerResult> {
187
+ if (!isWindowType(params.type)) {
188
+ return {
189
+ ok: false,
190
+ text: `Tipo de ventana desconocido "${params.type}". Tipos válidos: ${WINDOW_TYPES.join(", ")}.`,
191
+ details: { isError: true, action: "launch" },
192
+ };
193
+ }
194
+ if (params.location && !isSplitLocation(params.location)) {
195
+ return {
196
+ ok: false,
197
+ text: `Ubicación de split desconocida "${params.location}". Ubicaciones válidas: ${SPLIT_LOCATIONS.join(", ")}.`,
198
+ details: { isError: true, action: "launch" },
199
+ };
200
+ }
201
+ const result = await run(
202
+ buildLaunchArgs({ type: params.type, location: params.location as SplitLocation | undefined, cwd: params.cwd }),
203
+ opts,
204
+ );
205
+ if (!result.ok) {
206
+ return { ok: false, text: describeError(result, "launch"), details: { isError: true, action: "launch" } };
207
+ }
208
+ const id = result.stdout.trim();
209
+ return {
210
+ ok: true,
211
+ text: `Se abrió ${params.type} nueva (id ${id || "?"}).`,
212
+ details: { action: "launch", type: params.type, location: params.location, id },
213
+ };
214
+ }
215
+
216
+ export async function runGotoLayout(
217
+ run: RunKitty,
218
+ params: { layout: string },
219
+ opts: HandlerOpts,
220
+ ): Promise<HandlerResult> {
221
+ if (!params.layout) {
222
+ return {
223
+ ok: false,
224
+ text: "goto-layout requiere un nombre de layout.",
225
+ details: { isError: true, action: "goto-layout" },
226
+ };
227
+ }
228
+ const result = await run(buildGotoLayoutArgs(params.layout), opts);
229
+ if (!result.ok) {
230
+ return {
231
+ ok: false,
232
+ text: describeError(result, "goto-layout"),
233
+ details: { isError: true, action: "goto-layout" },
234
+ };
235
+ }
236
+ return {
237
+ ok: true,
238
+ text: `Layout activo: ${params.layout}.`,
239
+ details: { action: "goto-layout", layout: params.layout },
240
+ };
241
+ }
242
+
243
+ export async function runCloseWindow(
244
+ run: RunKitty,
245
+ params: { matchId?: string },
246
+ opts: HandlerOpts,
247
+ ): Promise<HandlerResult> {
248
+ const result = await run(buildCloseWindowArgs(params), opts);
249
+ if (!result.ok) {
250
+ return {
251
+ ok: false,
252
+ text: describeError(result, "close-window"),
253
+ details: { isError: true, action: "close-window" },
254
+ };
255
+ }
256
+ return {
257
+ ok: true,
258
+ text: params.matchId ? `Se cerró la ventana ${params.matchId}.` : "Se cerró la ventana activa.",
259
+ details: { action: "close-window", matchId: params.matchId },
260
+ };
261
+ }
262
+
263
+ export async function runFocusWindow(
264
+ run: RunKitty,
265
+ params: { matchId: string },
266
+ opts: HandlerOpts,
267
+ ): Promise<HandlerResult> {
268
+ if (!params.matchId) {
269
+ return {
270
+ ok: false,
271
+ text: "focus-window requiere un id de ventana.",
272
+ details: { isError: true, action: "focus-window" },
273
+ };
274
+ }
275
+ const result = await run(buildFocusWindowArgs(params.matchId), opts);
276
+ if (!result.ok) {
277
+ return {
278
+ ok: false,
279
+ text: describeError(result, "focus-window"),
280
+ details: { isError: true, action: "focus-window" },
281
+ };
282
+ }
283
+ return {
284
+ ok: true,
285
+ text: `Foco en la ventana ${params.matchId}.`,
286
+ details: { action: "focus-window", matchId: params.matchId },
287
+ };
288
+ }
package/notify.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+
3
+ export function notify(ctx: ExtensionContext, message: string, type: "info" | "warning" | "error" = "info"): void {
4
+ if (ctx.mode === "print") {
5
+ (type === "info" ? console.log : console.error)(message);
6
+ return;
7
+ }
8
+ if (ctx.hasUI) {
9
+ ctx.ui.notify(message, type);
10
+ return;
11
+ }
12
+ if (type !== "info") console.error(message);
13
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@pandi-coding-agent/pandi-kitty",
3
+ "version": "0.1.0",
4
+ "description": "Extensión de Pi para controlar el terminal kitty vía su protocolo de control remoto: un comando /kitty y una herramienta kitty_remote invocable por el modelo (tabs, ventanas, splits, layout).",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/andrestobelem/pandi-extensions.git",
9
+ "directory": "extensions/pandi-kitty"
10
+ },
11
+ "type": "module",
12
+ "keywords": [
13
+ "pi-package",
14
+ "pi",
15
+ "pi-extension",
16
+ "kitty",
17
+ "terminal",
18
+ "remote-control"
19
+ ],
20
+ "files": [
21
+ "index.ts",
22
+ "command.ts",
23
+ "kitty.ts",
24
+ "notify.ts",
25
+ "skills",
26
+ "README.md"
27
+ ],
28
+ "pi": {
29
+ "extensions": [
30
+ "./index.ts"
31
+ ],
32
+ "skills": [
33
+ "./skills"
34
+ ]
35
+ },
36
+ "peerDependencies": {
37
+ "@earendil-works/pi-ai": "^0.80.3",
38
+ "@earendil-works/pi-coding-agent": "^0.80.3",
39
+ "typebox": "^1.1.38"
40
+ }
41
+ }
@@ -0,0 +1,81 @@
1
+ ---
2
+ name: kitty-remote-control
3
+ description: >-
4
+ Controla la terminal kitty desde el shell mediante su protocolo de
5
+ remote-control (`kitty @ ...`): abre tabs, ventanas y splits nuevos, y
6
+ consulta/administra la instancia de kitty en ejecución. Usar cuando el
7
+ usuario pida abrir una tab o ventana nueva de kitty, dividir paneles
8
+ (vertical/horizontal), o scriptear kitty desde una sesión en curso. Si la
9
+ extensión pandi-kitty está instalada, preferí su comando `/kitty` o la tool
10
+ `kitty_remote` antes que shell ad hoc.
11
+ ---
12
+
13
+ # Control remoto de kitty
14
+
15
+ kitty trae un protocolo de remote-control manejado por el subcomando
16
+ `kitty @`. Habla con la instancia de kitty *actualmente en ejecución* a
17
+ través de un socket de control, así que solo funciona desde dentro de una
18
+ sesión kitty que tenga el remote control habilitado. 🐼
19
+
20
+ ## Prerequisito
21
+
22
+ El remote control tiene que estar permitido. Revisá/habilitalo en `kitty.conf`:
23
+
24
+ ```
25
+ allow_remote_control yes
26
+ ```
27
+
28
+ O iniciá kitty con `-o allow_remote_control=yes`. Sin esto, todo comando
29
+ `kitty @ ...` falla con un error de socket/permisos.
30
+
31
+ ## Comandos principales
32
+
33
+ ```bash
34
+ # Tab nueva en la ventana actual
35
+ kitty @ launch --type=tab
36
+
37
+ # Ventana de OS nueva
38
+ kitty @ launch --type=os-window
39
+
40
+ # Ventana nueva en la tab actual (usa el layout activo)
41
+ kitty @ launch --type=window
42
+
43
+ # Split vertical (solo tiene efecto bajo el layout `splits`)
44
+ kitty @ launch --type=window --location=vsplit
45
+
46
+ # Split horizontal
47
+ kitty @ launch --type=window --location=hsplit
48
+
49
+ # Forzar el layout splits primero si el layout activo no es splits
50
+ kitty @ goto-layout splits
51
+ ```
52
+
53
+ Cada llamada a `launch` imprime el id de la tab/ventana nueva si tiene éxito
54
+ (p. ej. `2`), que se puede usar con otros subcomandos de `kitty @`
55
+ (`focus-window`, `close-window`, `send-text`, etc.) para apuntarle con
56
+ precisión.
57
+
58
+ ## Trampas
59
+
60
+ - `--location=vsplit`/`hsplit` solo tiene efecto visible bajo el layout
61
+ `splits`. Bajo `tall`, `fat`, `grid`, etc. la ventana nueva igual se abre,
62
+ pero la dirección del split la decide ese layout en su lugar.
63
+ - Todos los comandos `kitty @` son no-ops desde fuera de kitty (por ejemplo,
64
+ desde un script desacoplado o una terminal distinta) — necesitan que el
65
+ socket de un proceso kitty vivo sea alcanzable, lo cual normalmente
66
+ significa correr desde un shell dentro de esa instancia kitty.
67
+ - `--type=window` sin `--location` simplemente usa lo que el layout actual
68
+ haga con una ventana nueva (puede no verse como un "split" en absoluto).
69
+
70
+ ## Integración en Pi
71
+
72
+ En este repo, `extensions/pandi-kitty/` envuelve estos comandos como paquete
73
+ standalone:
74
+
75
+ - comando `/kitty` para personas (`tab`, `window`, `vsplit`, `hsplit`,
76
+ `os-window`, `layout`, `close`, `focus`)
77
+ - tool `kitty_remote` para el modelo, con acciones `launch`, `goto-layout`,
78
+ `close-window` y `focus-window`
79
+
80
+ Usá la extensión cuando esté disponible; reservá `kitty @ ...` directo para
81
+ casos de diagnóstico o cuando la extensión no esté instalada.