frida-pi-studio 0.6.1
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/CHANGELOG.md +100 -0
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/assets/logo.png +0 -0
- package/assets/logo.svg +35 -0
- package/docs/feature-catalog.md +148 -0
- package/docs/theme-gallery.html +197 -0
- package/package.json +126 -0
- package/packages/setup/bin/setup.mjs +430 -0
- package/packages/studio/extensions/branding.ts +153 -0
- package/packages/studio/extensions/frida-eval.ts +135 -0
- package/packages/studio/extensions/frida-footer.ts +98 -0
- package/packages/studio/extensions/frida-uris.ts +148 -0
- package/packages/studio/prompts/.gitkeep +0 -0
- package/packages/studio/prompts/rol-datos-ia.md +19 -0
- package/packages/studio/prompts/rol-dev.md +20 -0
- package/packages/studio/prompts/rol-fullstack.md +20 -0
- package/packages/studio/prompts/rol-seguridad.md +19 -0
- package/packages/studio/skills/.gitkeep +0 -0
- package/packages/studio/skills/gh-quoting/SKILL.md +49 -0
- package/packages/studio/skills/learn-review/SKILL.md +58 -0
- package/packages/studio/themes/arctic-aurora.json +84 -0
- package/packages/studio/themes/aurora-circuit.json +85 -0
- package/packages/studio/themes/frida.json +48 -0
- package/packages/studio/themes/violet-breakwater.json +83 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FRIDA-STUDIO branding — reemplaza el header nativo de pi (logo +
|
|
3
|
+
* hints) por el banner FRIDA-STUDIO. API oficial: ctx.ui.setHeader().
|
|
4
|
+
*
|
|
5
|
+
* ⚠️ El arte NO se edita a mano — se regenera y verifica con:
|
|
6
|
+
* npx figlet -f "ANSI Shadow" -w 140 "FRIDA-STUDIO"
|
|
7
|
+
* (lección SOSIP4: dibujado a mano salió "SOSIP4"; y re-sucia si el
|
|
8
|
+
* bloque se re-escribe entero — editar SIN tocar LOGO).
|
|
9
|
+
* KIT_VERSION la sincroniza scripts/sync-kit.mjs desde package.json.
|
|
10
|
+
*
|
|
11
|
+
* Banner responsivo (#26): render(width) adapta — ancho completo
|
|
12
|
+
* muestra el arte; terminal estrecha recibe banner compacto que
|
|
13
|
+
* AVISA (nunca se rompe en silencio ni se trunca el arte).
|
|
14
|
+
* /frida (#25): panel de identidad del kit (versión, theme, modelo).
|
|
15
|
+
* Banner personalizable (#31): ${PI_CODING_AGENT_DIR:-~/.pi/agent}/
|
|
16
|
+
* frida/banner.txt del usuario reemplaza el arte (subtítulo y hints
|
|
17
|
+
* se conservan); el arte default sigue verificado con figlet.
|
|
18
|
+
*/
|
|
19
|
+
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import os from "node:os";
|
|
23
|
+
|
|
24
|
+
export const KIT_VERSION = "0.6.0";
|
|
25
|
+
|
|
26
|
+
const LOGO = [
|
|
27
|
+
"███████╗██████╗ ██╗██████╗ █████╗ ███████╗████████╗██╗ ██╗██████╗ ██╗ ██████╗ ",
|
|
28
|
+
"██╔════╝██╔══██╗██║██╔══██╗██╔══██╗ ██╔════╝╚══██╔══╝██║ ██║██╔══██╗██║██╔═══██╗",
|
|
29
|
+
"█████╗ ██████╔╝██║██║ ██║███████║█████╗███████╗ ██║ ██║ ██║██║ ██║██║██║ ██║",
|
|
30
|
+
"██╔══╝ ██╔══██╗██║██║ ██║██╔══██║╚════╝╚════██║ ██║ ██║ ██║██║ ██║██║██║ ██║",
|
|
31
|
+
"██║ ██║ ██║██║██████╔╝██║ ██║ ███████║ ██║ ╚██████╔╝██████╔╝██║╚██████╔╝",
|
|
32
|
+
"╚═╝ ╚═╝ ╚═╝╚═╝╚═════╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═════╝ ",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const HINTS =
|
|
36
|
+
" / comandos · ! bash · Ctrl+P modelos · Ctrl+T thinking · ? ayuda de pi";
|
|
37
|
+
|
|
38
|
+
// #31: banner del usuario (cacheado por sesión). null = usar LOGO.
|
|
39
|
+
let bannerCache: string[] | null | undefined;
|
|
40
|
+
function userBanner(): string[] | null {
|
|
41
|
+
if (bannerCache !== undefined) return bannerCache;
|
|
42
|
+
const dir =
|
|
43
|
+
process.env.PI_CODING_AGENT_DIR ?? join(os.homedir(), ".pi", "agent");
|
|
44
|
+
try {
|
|
45
|
+
const txt = readFileSync(join(dir, "frida", "banner.txt"), "utf8");
|
|
46
|
+
const lines = txt.split("\n").filter((l) => l.trim().length > 0);
|
|
47
|
+
bannerCache = lines.length > 0 ? lines : null;
|
|
48
|
+
} catch {
|
|
49
|
+
bannerCache = null;
|
|
50
|
+
}
|
|
51
|
+
return bannerCache;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function setFridaHeader(
|
|
55
|
+
ctx: { ui: { setHeader: Function } },
|
|
56
|
+
subtitle: string,
|
|
57
|
+
): void {
|
|
58
|
+
ctx.ui.setHeader((_tui: unknown, theme: Theme) => ({
|
|
59
|
+
render(width: number): string[] {
|
|
60
|
+
const banner = userBanner() ?? LOGO;
|
|
61
|
+
const need = Math.max(...banner.map((l) => l.length));
|
|
62
|
+
if (width >= need) {
|
|
63
|
+
// Layout completo: arte (default verificado o banner del
|
|
64
|
+
// usuario) + subtítulo + hints.
|
|
65
|
+
const art = banner.map((line) => theme.fg("accent", line));
|
|
66
|
+
const hints = theme.fg("dim", HINTS);
|
|
67
|
+
return [...art, subtitle, hints];
|
|
68
|
+
}
|
|
69
|
+
// Terminal estrecha: banner compacto que AVISA (patrón pi-kaush —
|
|
70
|
+
// degradar honestamente; el arte jamás se trunca ni se edita).
|
|
71
|
+
const compact = theme.fg("accent", `FRIDA-STUDIO v${KIT_VERSION}`);
|
|
72
|
+
const note = theme.fg(
|
|
73
|
+
"dim",
|
|
74
|
+
`arte completo requiere ${need} columnas (terminal: ${width}) — /frida identidad del kit`,
|
|
75
|
+
);
|
|
76
|
+
return [compact, note];
|
|
77
|
+
},
|
|
78
|
+
invalidate() {},
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function activeThemeName(): string {
|
|
83
|
+
const candidates = [
|
|
84
|
+
join(os.homedir(), ".pi", "agent", "settings.json"),
|
|
85
|
+
join(process.cwd(), ".pi", "settings.json"),
|
|
86
|
+
];
|
|
87
|
+
for (const p of candidates) {
|
|
88
|
+
try {
|
|
89
|
+
const theme = JSON.parse(readFileSync(p, "utf8")).theme;
|
|
90
|
+
if (typeof theme === "string" && theme) return theme;
|
|
91
|
+
} catch {
|
|
92
|
+
/* sin settings o ilegible — probar siguiente */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return "default de pi";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function fridaPanel(ctx: Record<string, unknown>, theme?: Theme): string[] {
|
|
99
|
+
const dim = (s: string) => (theme ? theme.fg("dim", s) : s);
|
|
100
|
+
const accent = (s: string) => (theme ? theme.fg("accent", s) : s);
|
|
101
|
+
const model =
|
|
102
|
+
typeof ctx.model === "string"
|
|
103
|
+
? ctx.model
|
|
104
|
+
: (ctx.model as { id?: string } | undefined)?.id;
|
|
105
|
+
return [
|
|
106
|
+
accent(`FRIDA-STUDIO v${KIT_VERSION} — identidad del kit`),
|
|
107
|
+
dim(`theme activo: ${activeThemeName()}`),
|
|
108
|
+
dim(`modelo: ${model ?? "(no disponible aún)"}`),
|
|
109
|
+
dim("footer: FRIDA footer con marca y telemetría (extensión propia)"),
|
|
110
|
+
dim("actualizar: pi update --extensions · docs: docs/feature-catalog.md"),
|
|
111
|
+
dim("(/frida de nuevo para refrescar)"),
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default function (pi: ExtensionAPI): void {
|
|
116
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
117
|
+
if (ctx.mode !== "tui") return;
|
|
118
|
+
const subtitle =
|
|
119
|
+
(ctx as { theme?: Theme }).theme?.fg("muted", " FRIDA STUDIO") ??
|
|
120
|
+
" FRIDA STUDIO";
|
|
121
|
+
setFridaHeader(ctx as never, `${subtitle} v${KIT_VERSION} · powered by pi`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
pi.registerCommand("header-frida", {
|
|
125
|
+
description: "Restaurar el header FRIDA-STUDIO",
|
|
126
|
+
handler: async (_args, ctx) => {
|
|
127
|
+
setFridaHeader(
|
|
128
|
+
ctx as never,
|
|
129
|
+
` FRIDA STUDIO v${KIT_VERSION} · powered by pi`,
|
|
130
|
+
);
|
|
131
|
+
ctx.ui.notify("Header FRIDA-STUDIO restaurado", "info");
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
pi.registerCommand("header-pi", {
|
|
136
|
+
description: "Restaurar el header nativo de pi",
|
|
137
|
+
handler: async (_args, ctx) => {
|
|
138
|
+
ctx.ui.setHeader(undefined);
|
|
139
|
+
ctx.ui.notify("Header nativo de pi restaurado", "info");
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
pi.registerCommand("frida", {
|
|
144
|
+
description: "Identidad del kit: versión, theme, modelo y cómo actualizar",
|
|
145
|
+
handler: async (_args, ctx) => {
|
|
146
|
+
if (!ctx.hasUI) return;
|
|
147
|
+
ctx.ui.setWidget(
|
|
148
|
+
"frida",
|
|
149
|
+
fridaPanel(ctx as never, (ctx as { theme?: Theme }).theme),
|
|
150
|
+
);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FRIDA eval (#21 v1) — kernel JS persistente para el agente: lo que
|
|
3
|
+
* se define en una llamada (variables, funciones) vive en la
|
|
4
|
+
* siguiente, dentro de la misma sesión de pi. Casos: munging de JSON,
|
|
5
|
+
* cálculos, transformación de datos — sin salir del chat.
|
|
6
|
+
*
|
|
7
|
+
* Alcance v1 (veredicto en #21): JS vía node:vm (in-process, contexto
|
|
8
|
+
* aislado del host de pi, timeout 10s). Python persistente y el
|
|
9
|
+
* re-entry directo a las tools del agente dependen de upstream pi
|
|
10
|
+
* (stock no expone invocar tools desde código de extensión) —
|
|
11
|
+
* documentado, no intentado aquí.
|
|
12
|
+
*
|
|
13
|
+
* Seguridad: mismo modelo que la tool bash — ejecuta código a
|
|
14
|
+
* petición del agente en la máquina del usuario; vm aísla del proceso
|
|
15
|
+
* de pi (sin require/process/Buffer en el sandbox).
|
|
16
|
+
*/
|
|
17
|
+
// @ts-ignore — builtin de node provisto por pi en runtime; el shim de
|
|
18
|
+
// de types.d.ts tipa para tsc. ts-ignore (NO expect-error, ver
|
|
19
|
+
// biome.json noTsIgnore:off): el LSP sin tsconfig lo marca, tsc no.
|
|
20
|
+
import * as vm from "node:vm";
|
|
21
|
+
import { Type } from "typebox";
|
|
22
|
+
|
|
23
|
+
const TIMEOUT_MS = 10_000;
|
|
24
|
+
|
|
25
|
+
interface EvalToolResult {
|
|
26
|
+
content: { type: "text"; text: string }[];
|
|
27
|
+
details: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface EvalExtensionAPI {
|
|
31
|
+
registerTool: (def: {
|
|
32
|
+
name: string;
|
|
33
|
+
label: string;
|
|
34
|
+
description: string;
|
|
35
|
+
promptGuidelines?: string[];
|
|
36
|
+
parameters: unknown;
|
|
37
|
+
execute: (
|
|
38
|
+
toolCallId: string,
|
|
39
|
+
params: { code: string; reset?: boolean },
|
|
40
|
+
signal: unknown,
|
|
41
|
+
onUpdate: unknown,
|
|
42
|
+
ctx: unknown,
|
|
43
|
+
) => Promise<EvalToolResult>;
|
|
44
|
+
}) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fmt(value: unknown): string {
|
|
48
|
+
if (typeof value === "string") return value;
|
|
49
|
+
if (value === undefined) return "undefined";
|
|
50
|
+
if (typeof value === "function") {
|
|
51
|
+
return `[Function: ${(value as { name?: string }).name || "anónima"}]`;
|
|
52
|
+
}
|
|
53
|
+
if (typeof value === "object" && value !== null) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.stringify(value, null, 2) ?? String(value);
|
|
56
|
+
} catch {
|
|
57
|
+
return String(value);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return String(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default function (pi: EvalExtensionAPI): void {
|
|
64
|
+
let context: vm.Context | undefined;
|
|
65
|
+
|
|
66
|
+
function ensureContext(reset: boolean): vm.Context {
|
|
67
|
+
if (!reset && context) return context;
|
|
68
|
+
const logs: string[] = [];
|
|
69
|
+
const sandbox = {
|
|
70
|
+
console: {
|
|
71
|
+
log: (...a: unknown[]) => logs.push(a.map((x) => fmt(x)).join(" ")),
|
|
72
|
+
error: (...a: unknown[]) =>
|
|
73
|
+
logs.push(`✗ ${a.map((x) => fmt(x)).join(" ")}`),
|
|
74
|
+
},
|
|
75
|
+
__logs: logs,
|
|
76
|
+
};
|
|
77
|
+
context = vm.createContext(sandbox);
|
|
78
|
+
return context;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pi.registerTool({
|
|
82
|
+
name: "eval_js",
|
|
83
|
+
label: "Eval JS (kernel persistente)",
|
|
84
|
+
description:
|
|
85
|
+
"Ejecuta JavaScript en un kernel PERSISTENTE por sesión: las" +
|
|
86
|
+
" variables y funciones definidas en una llamada están" +
|
|
87
|
+
" disponibles en la siguiente. Para munging de datos, cálculos" +
|
|
88
|
+
" y transformaciones sin salir del chat. Usa `reset: true` para" +
|
|
89
|
+
" empezar de cero.",
|
|
90
|
+
promptGuidelines: [
|
|
91
|
+
"Usa eval_js para cálculos, transformación de JSON y munging de" +
|
|
92
|
+
" datos — el estado persiste entre llamadas (declara helpers" +
|
|
93
|
+
" una vez y reúsalos).",
|
|
94
|
+
],
|
|
95
|
+
parameters: Type.Object({
|
|
96
|
+
code: Type.String({ description: "Código JS a ejecutar" }),
|
|
97
|
+
reset: Type.Optional(
|
|
98
|
+
Type.Boolean({ description: "Reiniciar el kernel (default false)" }),
|
|
99
|
+
),
|
|
100
|
+
}),
|
|
101
|
+
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
102
|
+
let logs: string[] = [];
|
|
103
|
+
try {
|
|
104
|
+
const ctx = ensureContext(params.reset === true);
|
|
105
|
+
// SAFETY: __logs está garantizado en el contexto porque lo
|
|
106
|
+
// inyectamos nosostros en ensureContext (única fábrica de
|
|
107
|
+
// contextos); TypeScript no puede verlo porque vm.Context es
|
|
108
|
+
// opaco por diseño del shim.
|
|
109
|
+
logs = (ctx as unknown as { __logs?: string[] }).__logs ?? [];
|
|
110
|
+
logs.length = 0;
|
|
111
|
+
const value = new vm.Script(params.code, {
|
|
112
|
+
filename: "eval_js",
|
|
113
|
+
}).runInContext(ctx, { timeout: TIMEOUT_MS });
|
|
114
|
+
const parts: string[] = [];
|
|
115
|
+
if (logs.length > 0) parts.push(logs.join("\n"));
|
|
116
|
+
if (value !== undefined) parts.push(`⇒ ${fmt(value)}`);
|
|
117
|
+
if (parts.length === 0) parts.push("(sin salida — ¿retornar algo?)");
|
|
118
|
+
return {
|
|
119
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
120
|
+
details: { kernel: "js", persisted: params.reset !== true },
|
|
121
|
+
};
|
|
122
|
+
} catch (e) {
|
|
123
|
+
return {
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: "text",
|
|
127
|
+
text: `✗ ${e instanceof Error ? `${e.name}: ${e.message}` : String(e)}`,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
details: { kernel: "js", error: true },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FRIDA footer (#8) — status bar con marca + telemetría básica:
|
|
3
|
+
* FRIDA-STUDIO vX · modelo · % de contexto · rama git. Extensión
|
|
4
|
+
* PROPIA (regla de vehículos: el footer necesita marca FRIDA);
|
|
5
|
+
* reemplaza a pi-powerline-footer en el bundle para no pelear por
|
|
6
|
+
* el único slot del footer (quien prefiera powerline puede
|
|
7
|
+
* instalarlo aparte: pi install npm:pi-powerline-footer).
|
|
8
|
+
*
|
|
9
|
+
* API: ctx.ui.setStatus(id, text) — refresco cada 30s y al arrancar
|
|
10
|
+
* (patrón monopi custom-footer). KIT_VERSION se importa de
|
|
11
|
+
* branding.ts (fuente única que sincroniza sync-kit.mjs).
|
|
12
|
+
*/
|
|
13
|
+
import { KIT_VERSION } from "./branding";
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
const REFRESH_MS = 30_000;
|
|
17
|
+
|
|
18
|
+
declare const process: { cwd(): string };
|
|
19
|
+
|
|
20
|
+
// Tipos estructurales locales (el repo no instala los peers de pi en
|
|
21
|
+
// node_modules — regla de aislamiento; el loader de pi los provee en
|
|
22
|
+
// runtime y los tipos de aquí solo documentan la superficie usada).
|
|
23
|
+
interface FooterCtx {
|
|
24
|
+
mode?: string;
|
|
25
|
+
cwd?: string;
|
|
26
|
+
model?: string | { id?: string };
|
|
27
|
+
getContextUsage?: () => unknown;
|
|
28
|
+
ui?: { setStatus?: (id: string, text: string) => void };
|
|
29
|
+
}
|
|
30
|
+
interface FooterExtensionAPI {
|
|
31
|
+
on: (
|
|
32
|
+
event: "session_start",
|
|
33
|
+
cb: (event: unknown, ctx: FooterCtx) => void | Promise<void>,
|
|
34
|
+
) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function gitBranch(cwd: string): string {
|
|
38
|
+
try {
|
|
39
|
+
const head = readFileSync(join(cwd, ".git", "HEAD"), "utf8").trim();
|
|
40
|
+
const m = head.match(/^ref: refs\/heads\/(.+)$/);
|
|
41
|
+
if (m) return m[1];
|
|
42
|
+
if (head.length === 40) return `(detached ${head.slice(0, 7)})`;
|
|
43
|
+
} catch {
|
|
44
|
+
/* no es un repo git — sin rama */
|
|
45
|
+
}
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function contextPct(ctx: FooterCtx): string {
|
|
50
|
+
try {
|
|
51
|
+
const usage = (ctx.getContextUsage as () => unknown)();
|
|
52
|
+
if (typeof usage === "number") return `${Math.round(usage)}%`;
|
|
53
|
+
if (usage && typeof usage === "object") {
|
|
54
|
+
const u = usage as Record<string, unknown>;
|
|
55
|
+
for (const k of ["usedPct", "percent", "pct", "usedPercent"]) {
|
|
56
|
+
if (typeof u[k] === "number") return `${Math.round(u[k] as number)}%`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
/* API distinta entre versiones de pi — omitir sin romper */
|
|
61
|
+
}
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderFooter(ctx: FooterCtx): string {
|
|
66
|
+
const parts = [`FRIDA-STUDIO v${KIT_VERSION}`];
|
|
67
|
+
const model = typeof ctx.model === "string" ? ctx.model : ctx.model?.id;
|
|
68
|
+
if (model) parts.push(model);
|
|
69
|
+
const pct = contextPct(ctx);
|
|
70
|
+
if (pct) parts.push(`ctx ${pct}`);
|
|
71
|
+
const branch = gitBranch(
|
|
72
|
+
typeof ctx.cwd === "string" ? ctx.cwd : process.cwd(),
|
|
73
|
+
);
|
|
74
|
+
if (branch) parts.push(branch);
|
|
75
|
+
return parts.join(" · ");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default function (pi: FooterExtensionAPI): void {
|
|
79
|
+
const tick = (ctx: FooterCtx) => {
|
|
80
|
+
try {
|
|
81
|
+
ctx.ui?.setStatus?.("frida-footer", renderFooter(ctx));
|
|
82
|
+
} catch {
|
|
83
|
+
/* sin UI o API distinta — silent */
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
pi.on("session_start", async (_event: unknown, ctx: FooterCtx) => {
|
|
88
|
+
if (ctx.mode !== "tui") return;
|
|
89
|
+
tick(ctx);
|
|
90
|
+
// SAFETY: en runtime (node) setInterval devuelve un Timeout con
|
|
91
|
+
// unref(); el tipo number viene de la lib DOM del linter, que no
|
|
92
|
+
// conoce los timers de node (repo sin @types/node por aislamiento).
|
|
93
|
+
const timer = setInterval(() => tick(ctx), REFRESH_MS) as unknown as {
|
|
94
|
+
unref: () => void;
|
|
95
|
+
};
|
|
96
|
+
timer.unref();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FRIDA URIs (#19) — recursos del equipo como si fueran archivos:
|
|
3
|
+
* `pr://N`, `issue://N`, `diff://N` — una sola interfaz para el
|
|
4
|
+
* modelo en vez de N flags de gh que aprender (idea de producto
|
|
5
|
+
* tomada de omp, adaptada al kit sobre pi stock).
|
|
6
|
+
*
|
|
7
|
+
* v1: GitHub del repo actual vía `gh` (el equipo ya viene
|
|
8
|
+
* autenticado). Follow-up documentado: `agent://<id>/...` vía el
|
|
9
|
+
* event bus/RPC de pi-subagents.
|
|
10
|
+
*/
|
|
11
|
+
// @ts-ignore — módulo builtin de node provisto por pi en runtime;
|
|
12
|
+
// resuelve vía el shim de types.d.ts para tsc (ver tsconfig studio).
|
|
13
|
+
// ts-ignore (no expect-error): el LSP sin tsconfig lo marca, tsc no.
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
import { Type } from "typebox";
|
|
16
|
+
|
|
17
|
+
const MAX_OUT = 50_000;
|
|
18
|
+
|
|
19
|
+
interface ToolResult {
|
|
20
|
+
content: { type: "text"; text: string }[];
|
|
21
|
+
details: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface UriToolCtx {
|
|
25
|
+
cwd?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface UriExtensionAPI {
|
|
29
|
+
registerTool: (def: {
|
|
30
|
+
name: string;
|
|
31
|
+
label: string;
|
|
32
|
+
description: string;
|
|
33
|
+
promptGuidelines?: string[];
|
|
34
|
+
parameters: unknown;
|
|
35
|
+
execute: (
|
|
36
|
+
toolCallId: string,
|
|
37
|
+
params: { uri: string },
|
|
38
|
+
signal: unknown,
|
|
39
|
+
onUpdate: unknown,
|
|
40
|
+
ctx: UriToolCtx,
|
|
41
|
+
) => Promise<ToolResult>;
|
|
42
|
+
}) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ghField(o: Record<string, unknown>, key: string): string {
|
|
46
|
+
const v = o[key];
|
|
47
|
+
if (v == null) return "";
|
|
48
|
+
if (typeof v === "object") {
|
|
49
|
+
const login = (v as { login?: string }).login;
|
|
50
|
+
if (login) return login;
|
|
51
|
+
return JSON.stringify(v);
|
|
52
|
+
}
|
|
53
|
+
return String(v);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function prettyJson(json: string, kind: "pr" | "issue"): string {
|
|
57
|
+
try {
|
|
58
|
+
const o = JSON.parse(json) as Record<string, unknown>;
|
|
59
|
+
const head =
|
|
60
|
+
kind === "pr"
|
|
61
|
+
? `# PR #${ghField(o, "number")}: ${ghField(o, "title")}`
|
|
62
|
+
: `# Issue #${ghField(o, "number")}: ${ghField(o, "title")}`;
|
|
63
|
+
const state = `**estado:** ${ghField(o, "state")} · **autor:** ${ghField(o, "author")}`;
|
|
64
|
+
const extra =
|
|
65
|
+
kind === "pr"
|
|
66
|
+
? ` · +${ghField(o, "additions")}/-${ghField(o, "deletions")} · ${ghField(o, "changedFiles")} archivos`
|
|
67
|
+
: "";
|
|
68
|
+
const body = ghField(o, "body") || "(sin cuerpo)";
|
|
69
|
+
return `${head}\n\n${state}${extra}\n\n---\n\n${body}`;
|
|
70
|
+
} catch {
|
|
71
|
+
return json;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default function (pi: UriExtensionAPI): void {
|
|
76
|
+
pi.registerTool({
|
|
77
|
+
name: "read_uri",
|
|
78
|
+
label: "Leer URI del equipo",
|
|
79
|
+
description:
|
|
80
|
+
"Lee recursos del equipo como si fueran archivos: pr://N (pull" +
|
|
81
|
+
" request), issue://N (issue), diff://N (diff del PR). Una sola" +
|
|
82
|
+
" interfaz en vez de aprender comandos gh.",
|
|
83
|
+
promptGuidelines: [
|
|
84
|
+
"Usa read_uri (pr://N, issue://N, diff://N) para leer pull" +
|
|
85
|
+
" requests, issues y diffs del repo actual, en vez de llamar" +
|
|
86
|
+
" gh directo.",
|
|
87
|
+
],
|
|
88
|
+
parameters: Type.Object({
|
|
89
|
+
uri: Type.String({
|
|
90
|
+
description: "pr://N | issue://N | diff://N (N = número)",
|
|
91
|
+
}),
|
|
92
|
+
}),
|
|
93
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
94
|
+
const m = /^(pr|issue|diff):\/\/(\d+)$/.exec(params.uri.trim());
|
|
95
|
+
if (!m) {
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: `✗ URI no válida: "${params.uri}" — esperar pr://N, issue://N o diff://N`,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
details: { uri: params.uri, error: "bad-uri" },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const [, scheme, n] = m;
|
|
107
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
108
|
+
const args =
|
|
109
|
+
scheme === "pr"
|
|
110
|
+
? [
|
|
111
|
+
"pr",
|
|
112
|
+
"view",
|
|
113
|
+
n,
|
|
114
|
+
"--json",
|
|
115
|
+
"number,title,state,author,body,additions,deletions,changedFiles",
|
|
116
|
+
]
|
|
117
|
+
: scheme === "issue"
|
|
118
|
+
? [
|
|
119
|
+
"issue",
|
|
120
|
+
"view",
|
|
121
|
+
n,
|
|
122
|
+
"--json",
|
|
123
|
+
"number,title,state,author,body,labels",
|
|
124
|
+
]
|
|
125
|
+
: ["pr", "diff", n];
|
|
126
|
+
const r = spawnSync("gh", args, { encoding: "utf8", cwd });
|
|
127
|
+
if (r.status !== 0 || !r.stdout) {
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: "text",
|
|
132
|
+
text: `✗ gh exit ${r.status}: ${(r.stderr ?? "").slice(0, 500)}`,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
details: { uri: params.uri, error: "gh-failed" },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const out =
|
|
139
|
+
scheme === "diff"
|
|
140
|
+
? r.stdout
|
|
141
|
+
: prettyJson(r.stdout, scheme === "pr" ? "pr" : "issue");
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: "text", text: out.slice(0, MAX_OUT) }],
|
|
144
|
+
details: { uri: params.uri },
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rol datos/IA — pipelines de datos y ML reproducibles
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Actúa como **ingeniero de datos/IA** para esta tarea/sesión.
|
|
6
|
+
|
|
7
|
+
## Contrato
|
|
8
|
+
|
|
9
|
+
- Reproducibilidad primero: seeds fijas, versiones pinneadas, pasos deterministas documentados.
|
|
10
|
+
- Datos crudos inmutables; toda transformación es un paso explícito y re-ejecutable.
|
|
11
|
+
- Valida supuestos de los datos (nulos, duplicados, rangos, drift) antes de modelar nada.
|
|
12
|
+
- Experimentos con hipótesis y métrica declaradas ANTES de correr; sin p-hacking.
|
|
13
|
+
- Un artefacto de modelo sin métricas de evaluación y datos de prueba no es un entregable.
|
|
14
|
+
- Costo/latencia estimados para anything que vaya a producción.
|
|
15
|
+
|
|
16
|
+
## Defaults
|
|
17
|
+
|
|
18
|
+
- Notebooks para explorar, scripts para producir; nada crítico vive solo en un notebook.
|
|
19
|
+
- Logs estructurados por paso del pipeline (qué, cuánto, cuándo, resultado).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rol desarrollador general — defaults seguros para trabajo diario
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Actúa como **desarrollador general del equipo** para esta tarea/sesión.
|
|
6
|
+
|
|
7
|
+
## Contrato
|
|
8
|
+
|
|
9
|
+
- Lee antes de editar: entiende el contexto del archivo y sus tests.
|
|
10
|
+
- Cambios mínimos y quirúrgicos; nada de reformatteo ajeno al cambio.
|
|
11
|
+
- Cada afirmación sobre el código se verifica leyéndolo, no de memoria.
|
|
12
|
+
- Si un cambio toca más archivos de lo esperado, para y repórtalo antes de seguir.
|
|
13
|
+
- Corre los tests/verificaciones del repo después de cada cambio que afirmen algo.
|
|
14
|
+
- No marques nada como hecho si queda implementación parcial o errores.
|
|
15
|
+
|
|
16
|
+
## Defaults
|
|
17
|
+
|
|
18
|
+
- Sigue los AGENTS.md del camino raíz→archivo (contratos vinculantes).
|
|
19
|
+
- Commits atómicos con mensaje estructurado cuando se pida.
|
|
20
|
+
- Ante ambigüedad: pregunta con opciones concretas en vez de asumir.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rol fullstack — arquitectura y calidad de punta a punta
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Actúa como **desarrollador fullstack** para esta tarea/sesión.
|
|
6
|
+
|
|
7
|
+
## Contrato
|
|
8
|
+
|
|
9
|
+
- Piensa en el stack completo: datos → API → UI → despliegue.
|
|
10
|
+
- Antes de proponer, nombra las alternativas viables (máx. 3) y por qué descartas las otras.
|
|
11
|
+
- Cada decisión de arquitectura queda documentada: qué, por qué, trade-offs.
|
|
12
|
+
- Interfaces profundas y módulos con una sola responsabilidad; evita superficies anchas.
|
|
13
|
+
- Validación en capas: tipos/tests del backend, render/tests del frontend, humo del flujo completo.
|
|
14
|
+
- Backwards compatibility: un cambio de contrato se migra, no se rompe.
|
|
15
|
+
|
|
16
|
+
## Defaults
|
|
17
|
+
|
|
18
|
+
- Errores de usuario y de sistema por separado; nunca un `catch` vacío.
|
|
19
|
+
- Estado del servidor como fuente de verdad; el cliente es una vista.
|
|
20
|
+
- Migraciones y seeds versionados con el código.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Rol seguridad — auditoría y análisis de vulnerabilidades
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Actúa como **investigador de seguridad** para esta tarea/sesión.
|
|
6
|
+
|
|
7
|
+
## Contrato
|
|
8
|
+
|
|
9
|
+
- Solo en sistemas/alcance autorizados explícitamente; si no está autorizado, no se toca.
|
|
10
|
+
- Hallazgos con evidencia: archivo:línea, vector, impacto, repro/POC cuando aplique.
|
|
11
|
+
- Severidad honesta (crítica/alta/media/baja) con justificación, sin inflar.
|
|
12
|
+
- Distingue siempre riesgo real vs teórico vs defense-in-depth.
|
|
13
|
+
- Remediación accionable por hallazgo, ordenada por costo/beneficio.
|
|
14
|
+
- Secretos y credenciales jamás a logs, issues ni output — referencia, no valor.
|
|
15
|
+
|
|
16
|
+
## Defaults
|
|
17
|
+
|
|
18
|
+
- Empieza por la superficie expuesta (input handling, auth, deserialización, inyección, secretos en repo).
|
|
19
|
+
- False positives verificados antes de reportarse; cada claim se comprueba contra el código.
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gh-quoting
|
|
3
|
+
description: Construir comandos gh (issue/pr/release) con bodies o comentarios dinámicos sin que el shell ejecute backticks o $ del contenido. Usar siempre que se genere markdown con código como argumento de gh CLI.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# gh-quoting — argumentos seguros para gh CLI
|
|
7
|
+
|
|
8
|
+
Los bodies de issues/comentarios llevan markdown con backticks y `$` —
|
|
9
|
+
pasarlos como argumento de shell entre comillas dobles los EJECUTA
|
|
10
|
+
(sustitución de comando/variable) y el contenido queda mocho. El fallo
|
|
11
|
+
es **silencioso**: gh sale 0 con el texto corrupto. (Nacida de lecciones
|
|
12
|
+
reales del equipo vía learn-review — issue #33.)
|
|
13
|
+
|
|
14
|
+
## Cuándo usar
|
|
15
|
+
|
|
16
|
+
- Cualquier `gh` con contenido dinámico: `--body`, `--comment`,
|
|
17
|
+
`--title`, notas de release.
|
|
18
|
+
- Especialmente si el texto contiene backticks (código), `$(...)` o `$`.
|
|
19
|
+
|
|
20
|
+
## Procedimiento
|
|
21
|
+
|
|
22
|
+
1. NUNCA `--body "..."` con comillas dobles si el texto puede traer
|
|
23
|
+
backticks o `$` — el shell los ejecuta.
|
|
24
|
+
2. SIEMPRE heredoc quoted por stdin:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
gh issue create --body-file - <<'EOF'
|
|
28
|
+
...contenido con `backticks` y $vars intactos...
|
|
29
|
+
EOF
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
El quoting de `'EOF'` desactiva toda sustitución dentro del bloque.
|
|
33
|
+
|
|
34
|
+
3. Si el comando no acepta stdin: archivo temporal +
|
|
35
|
+
`--body-file /tmp/x.md`.
|
|
36
|
+
4. Tras publicar, releer el resultado (`gh issue view`) cuando el
|
|
37
|
+
contenido tenía backticks o `$`.
|
|
38
|
+
|
|
39
|
+
## Trampas
|
|
40
|
+
|
|
41
|
+
- Comillas simples anidadas dentro de `--body '...'` también rompen —
|
|
42
|
+
heredoc.
|
|
43
|
+
- `$(...)` en el contenido se ejecuta igual que los backticks.
|
|
44
|
+
- gh exit 0 con el texto mocho: no confiar en el exit code solo.
|
|
45
|
+
|
|
46
|
+
## Verificación
|
|
47
|
+
|
|
48
|
+
- El issue/comentario publicado conserva backticks y `$` intactos
|
|
49
|
+
(releído con `gh issue view`).
|