tribuchet: building on jamie treefmt v2.6.0traversed 1126 files emitted 455 files for processing formatted 455 files (2 changed) in 1.521s M home/.pi/agent/extensions/inbox.ts M home/.pi/agent/extensions/python/index.ts diff --git a/home/.pi/agent/extensions/inbox.ts b/home/.pi/agent/extensions/inbox.ts index 0b5f038..f693974 100644 --- a/home/.pi/agent/extensions/inbox.ts +++ b/home/.pi/agent/extensions/inbox.ts @@ -30,15 +30,25 @@ function parse(raw: string): Event { } export default function (pi: ExtensionAPI) { - const path = join(process.env.XDG_RUNTIME_DIR ?? "/tmp", "pi-inbox", `${process.pid}.sock`); + const path = join( + process.env.XDG_RUNTIME_DIR ?? "/tmp", + "pi-inbox", + `${process.pid}.sock`, + ); pi.registerMessageRenderer("inbox", (message, _options, theme) => { const ev = message.details as Event; let lines = ev.text.split("\n"); if (lines.length > TAIL_LINES) { - lines = [`… ${lines.length - TAIL_LINES} lines`, ...lines.slice(-TAIL_LINES)]; + lines = [ + `… ${lines.length - TAIL_LINES} lines`, + ...lines.slice(-TAIL_LINES), + ]; } - const title = theme.fg("accent", theme.bold(`▌inbox ${ev.source ?? ""}`.trimEnd())); + const title = theme.fg( + "accent", + theme.bold(`▌inbox ${ev.source ?? ""}`.trimEnd()), + ); return new Text(`${title}\n${theme.fg("muted", lines.join("\n"))}`, 0, 0); }); @@ -46,20 +56,27 @@ export default function (pi: ExtensionAPI) { mkdirSync(join(path, ".."), { recursive: true, mode: 0o700 }); rmSync(path, { force: true }); const server = createServer(async (conn) => { - const raw = (await text(conn).catch(() => "")).slice(0, MAX_BYTES); - const ev = parse(raw); - if (!ev.text) return; - const header = ev.source ? `[inbox: ${ev.source}]` : "[inbox]"; - pi.sendMessage( - { customType: "inbox", content: `${header}\n${ev.text}`, display: true, details: ev }, - { deliverAs: "followUp", triggerTurn: true }, - ); + const raw = (await text(conn).catch(() => "")).slice(0, MAX_BYTES); + const ev = parse(raw); + if (!ev.text) return; + const header = ev.source ? `[inbox: ${ev.source}]` : "[inbox]"; + pi.sendMessage( + { + customType: "inbox", + content: `${header}\n${ev.text}`, + display: true, + details: ev, + }, + { deliverAs: "followUp", triggerTurn: true }, + ); }).listen(path); process.env.PI_INBOX = path; pi.on("session_start", async (_event, ctx) => { const file = ctx.sessionManager.getSessionFile(); - process.env.PI_SESSION_ID = file ? basename(file, ".jsonl") : `pid-${process.pid}`; + process.env.PI_SESSION_ID = file + ? basename(file, ".jsonl") + : `pid-${process.pid}`; }); pi.on("session_shutdown", async () => { diff --git a/home/.pi/agent/extensions/python/index.ts b/home/.pi/agent/extensions/python/index.ts index ca86ebf..a841c8a 100644 --- a/home/.pi/agent/extensions/python/index.ts +++ b/home/.pi/agent/extensions/python/index.ts @@ -58,121 +58,127 @@ export default function (pi: ExtensionAPI) { registerTool(); }); - const registerTool = () => pi.registerTool({ - name: "python", - label: "python", - description: - "Execute Python code in a persistent interpreter: variables, imports and open files survive between calls for the whole session. " + - "The value of a trailing expression is echoed like in a REPL; use print() for anything else. " + - "matplotlib figures are returned as images. Available: polars, matplotlib, requests, plumbum, pexpect, pyelftools + stdlib. " + - `Output is truncated to the last ${DEFAULT_MAX_LINES} lines / ${ - formatSize(DEFAULT_MAX_BYTES) - }.`, - promptSnippet: - "Run Python in a persistent interpreter (state kept across calls; polars, matplotlib, requests, pexpect, pyelftools)", - promptGuidelines: [ - "Use python instead of bash for multi-step data work (JSON/CSV/logs/ELF), calculations, and anything where re-parsing input on every call would be wasteful; state persists, so load once and iterate.", - "Drive interactive programs (ssh, REPLs, debuggers, installers) with pexpect inside the python tool: `child = pexpect.spawn(cmd, encoding='utf-8')` persists across calls; always pass `timeout=` to expect().", - ...(process.env.PI_INBOX - ? [ - "Don't block on long waits in python: run them in a `threading.Thread` that calls the predefined `notify(text, source=\"python\")` when done. It arrives later as an `[inbox: ]` message.", - ] - : []), - ], - parameters: Type.Object({ - code: Type.String({ description: "Python source to execute" }), - timeout: Type.Optional( - Type.Number({ - description: "Timeout in seconds (interrupts the cell, keeps state)", - }), - ), - }), + const registerTool = () => + pi.registerTool({ + name: "python", + label: "python", + description: + "Execute Python code in a persistent interpreter: variables, imports and open files survive between calls for the whole session. " + + "The value of a trailing expression is echoed like in a REPL; use print() for anything else. " + + "matplotlib figures are returned as images. Available: polars, matplotlib, requests, plumbum, pexpect, pyelftools + stdlib. " + + `Output is truncated to the last ${DEFAULT_MAX_LINES} lines / ${ + formatSize(DEFAULT_MAX_BYTES) + }.`, + promptSnippet: + "Run Python in a persistent interpreter (state kept across calls; polars, matplotlib, requests, pexpect, pyelftools)", + promptGuidelines: [ + "Use python instead of bash for multi-step data work (JSON/CSV/logs/ELF), calculations, and anything where re-parsing input on every call would be wasteful; state persists, so load once and iterate.", + "Drive interactive programs (ssh, REPLs, debuggers, installers) with pexpect inside the python tool: `child = pexpect.spawn(cmd, encoding='utf-8')` persists across calls; always pass `timeout=` to expect().", + ...(process.env.PI_INBOX + ? [ + 'Don\'t block on long waits in python: run them in a `threading.Thread` that calls the predefined `notify(text, source="python")` when done. It arrives later as an `[inbox: ]` message.', + ] + : []), + ], + parameters: Type.Object({ + code: Type.String({ description: "Python source to execute" }), + timeout: Type.Optional( + Type.Number({ + description: + "Timeout in seconds (interrupts the cell, keeps state)", + }), + ), + }), - // Without this pi's fallback renders only the tool name, hiding the code. - renderCall(args, theme, context) { - const text = (context.lastComponent as Text | undefined) ?? - new Text("", 0, 0); - let out = theme.fg("toolTitle", theme.bold("python")); - if (args.timeout) out += theme.fg("muted", ` (timeout ${args.timeout}s)`); - const code = (args.code ?? "").trimEnd(); - if (code) out += "\n" + highlightCode(code, "python").join("\n"); - text.setText(out); - return text; - }, + // Without this pi's fallback renders only the tool name, hiding the code. + renderCall(args, theme, context) { + const text = (context.lastComponent as Text | undefined) ?? + new Text("", 0, 0); + let out = theme.fg("toolTitle", theme.bold("python")); + if (args.timeout) { + out += theme.fg("muted", ` (timeout ${args.timeout}s)`); + } + const code = (args.code ?? "").trimEnd(); + if (code) out += "\n" + highlightCode(code, "python").join("\n"); + text.setText(out); + return text; + }, - renderResult(result, options, theme, context) { - const text = (context.lastComponent as Text | undefined) ?? - new Text("", 0, 0); - const output = result.content - .filter((c) => c.type === "text") - .map((c) => (c as { text: string }).text) - .join("\n") - .trimEnd(); - const images = result.content.filter((c) => c.type === "image").length; - const lines = output ? output.split("\n") : []; - // Tail, not head: tracebacks and REPL results come last. - const shown = options.expanded ? lines : lines.slice(-10); - const color = context.isError ? "error" : "toolOutput"; - let out = theme.fg("muted", "─── output ───"); - if (lines.length > shown.length) { - out += theme.fg( - "muted", - `\n... (${lines.length - shown.length} earlier lines, `, - ) + keyHint("app.tools.expand", "to expand") + theme.fg("muted", ")"); - } - if (shown.length) { - out += "\n" + shown.map((l) => theme.fg(color, l)).join("\n"); - } - if (images) out += theme.fg("muted", `\n[${images} image(s)]`); - text.setText(out); - return text; - }, + renderResult(result, options, theme, context) { + const text = (context.lastComponent as Text | undefined) ?? + new Text("", 0, 0); + const output = result.content + .filter((c) => c.type === "text") + .map((c) => (c as { text: string }).text) + .join("\n") + .trimEnd(); + const images = result.content.filter((c) => c.type === "image").length; + const lines = output ? output.split("\n") : []; + // Tail, not head: tracebacks and REPL results come last. + const shown = options.expanded ? lines : lines.slice(-10); + const color = context.isError ? "error" : "toolOutput"; + let out = theme.fg("muted", "─── output ───"); + if (lines.length > shown.length) { + out += theme.fg( + "muted", + `\n... (${lines.length - shown.length} earlier lines, `, + ) + keyHint("app.tools.expand", "to expand") + theme.fg("muted", ")"); + } + if (shown.length) { + out += "\n" + shown.map((l) => theme.fg(color, l)).join("\n"); + } + if (images) out += theme.fg("muted", `\n[${images} image(s)]`); + text.setText(out); + return text; + }, - async execute(_id, params, signal, _onUpdate, ctx) { - kernel ??= new Kernel(python, ctx.cwd); - const ac = new AbortController(); - signal?.addEventListener("abort", () => ac.abort(), { once: true }); - let timedOut = false; - const timer = params.timeout - ? setTimeout(() => { - timedOut = true; - ac.abort(); - }, params.timeout * 1000) - : undefined; - const r = await kernel.exec(params.code, ac.signal).finally(() => - clearTimeout(timer) - ); - // Throw like bash so pi ends the run as "aborted" and sends queued prompts. - if (signal?.aborted) throw new Error("Command aborted"); - if (timedOut && r.error?.startsWith("KeyboardInterrupt")) { - r.error = - `Timed out after ${params.timeout}s (cell interrupted, interpreter state kept)`; - } + async execute(_id, params, signal, _onUpdate, ctx) { + kernel ??= new Kernel(python, ctx.cwd); + const ac = new AbortController(); + signal?.addEventListener("abort", () => ac.abort(), { once: true }); + let timedOut = false; + const timer = params.timeout + ? setTimeout(() => { + timedOut = true; + ac.abort(); + }, params.timeout * 1000) + : undefined; + const r = await kernel.exec(params.code, ac.signal).finally(() => + clearTimeout(timer) + ); + // Throw like bash so pi ends the run as "aborted" and sends queued prompts. + if (signal?.aborted) throw new Error("Command aborted"); + if (timedOut && r.error?.startsWith("KeyboardInterrupt")) { + r.error = + `Timed out after ${params.timeout}s (cell interrupted, interpreter state kept)`; + } - let text = r.stdout; - if (r.stderr) { - text += (text && !text.endsWith("\n") ? "\n" : "") + r.stderr; - } - if (r.result !== null) { - text += (text && !text.endsWith("\n") ? "\n" : "") + r.result; - } - if (r.error) text += (text && !text.endsWith("\n") ? "\n" : "") + r.error; - const t = truncateTail(text || (r.images.length ? "" : "(no output)")); - if (t.truncated) { - t.content = - `[output truncated: showing last ${t.outputLines} of ${t.totalLines} lines]\n${t.content}`; - } + let text = r.stdout; + if (r.stderr) { + text += (text && !text.endsWith("\n") ? "\n" : "") + r.stderr; + } + if (r.result !== null) { + text += (text && !text.endsWith("\n") ? "\n" : "") + r.result; + } + if (r.error) { + text += (text && !text.endsWith("\n") ? "\n" : "") + r.error; + } + const t = truncateTail(text || (r.images.length ? "" : "(no output)")); + if (t.truncated) { + t.content = + `[output truncated: showing last ${t.outputLines} of ${t.totalLines} lines]\n${t.content}`; + } - const content: ({ type: "text"; text: string } | { - type: "image"; - data: string; - mimeType: string; - })[] = []; - if (t.content) content.push({ type: "text", text: t.content }); - for (const data of r.images) { - content.push({ type: "image", data, mimeType: "image/png" }); - } - return { content, details: undefined, isError: r.error !== null }; - }, - }); + const content: ({ type: "text"; text: string } | { + type: "image"; + data: string; + mimeType: string; + })[] = []; + if (t.content) content.push({ type: "text", text: t.content }); + for (const data of r.images) { + content.push({ type: "image", data, mimeType: "image/png" }); + } + return { content, details: undefined, isError: r.error !== null }; + }, + }); }