@vamsitalupula/pi-debug 0.0.0-stage → 2.0.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/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Talupula Vamsi
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
11
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,104 @@
1
- # Temporary Holding Version
1
+ # pi-debug
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ This is a [Pi](https://pi.dev) extension for Pi extension developers. It enables running Typescript within the Node.js context of the Pi coding agent
4
+
5
+ <p align="center">
6
+ <img src="https://raw.githubusercontent.com/tanavamsikrishna/pi-debug/refs/heads/main/screenshot.png" width="500" alt="Alt text description">
7
+ </p>
8
+
9
+ ## Installation
10
+
11
+ To install the extension only in Pi's configuration directory (`~/.pi`) or local extension directories, `cd` into that folder and run:
12
+
13
+ ```sh
14
+ pi install --local npm:@vamsitalupula/pi-debug
15
+ ```
16
+
17
+ ## How to use?
18
+
19
+ 1. Create a new file by running `/pi-debug ./debug.ts` in Pi user prompt
20
+ 2. You should see a new file called `debug.ts` created in the current folder
21
+
22
+ ```ts
23
+ import type {
24
+ ExtensionAPI,
25
+ ExtensionContext,
26
+ } from "@earendil-works/pi-coding-agent";
27
+
28
+ export default function (pi: ExtensionAPI, getCtx: () => ExtensionContext) {
29
+ // Your code here
30
+ // return getCtx().getSystemPrompt();
31
+ return pi.getActiveTools();
32
+ }
33
+ ```
34
+
35
+ 3. Now run the file with `/pi-debug ./debug.ts`
36
+ 4. The data/object returned by the `default export function` is shown as a pi notification
37
+ 5. The package `@earendil-works/pi-coding-agent` is available in the node environment running *pi*. But if you need LSP/linting support in your editor, you need to point your editor tools to the install location of the package one way or an other. The easiest is to just run `pnpm i --save-dev @earendil-works/pi-coding-agent` in the project or local directory
38
+
39
+ ## AI tool
40
+
41
+ `pi-debug` is also a tool the model can call. Pass the whole module, the same shape as `TEMPLATE.ts`, not a function body. It runs on the same jiti instance as `/pi-debug`, and the return value is the tool result. The tool description includes the template.
42
+
43
+ ## Examples
44
+
45
+ **To see the runtime system prompt**
46
+
47
+ ```ts
48
+ export default function (pi: ExtensionAPI, getCtx: () => ExtensionContext) {
49
+ return getCtx().getSystemPrompt();
50
+ }
51
+ ```
52
+
53
+ **To log tool results to a file**
54
+
55
+ ```ts
56
+ import { appendFileSync } from "node:fs";
57
+
58
+ export default function (pi: ExtensionAPI, getCtx: () => ExtensionContext) {
59
+ pi.on("tool_result", async (event, ctx) => {
60
+ const logEntry = JSON.stringify({
61
+ timestamp: new Date().toISOString(),
62
+ toolName: event.toolName,
63
+ toolCallId: event.toolCallId,
64
+ isError: event.isError,
65
+ content: event.content,
66
+ }, null, 2);
67
+
68
+ appendFileSync("/tmp/pi-tool-results.log", logEntry + "\n");
69
+ });
70
+ }
71
+ ```
72
+
73
+ **To get assistant message details with decreasing cache reads**
74
+
75
+ ```ts
76
+ const ctx = getCtx();
77
+ const branch = ctx.sessionManager.getBranch();
78
+
79
+ const messages = branch
80
+ .filter((entry) => entry.type === "message")
81
+ .map((entry) => entry.message);
82
+
83
+ return messages
84
+ .slice(1)
85
+ .filter((msg, index) => {
86
+ if (msg.role !== "assistant" || !msg.usage) return false;
87
+
88
+ const prevMsg = messages[index];
89
+ const prevCache =
90
+ prevMsg?.role === "assistant"
91
+ ? (prevMsg.usage?.cacheRead ?? 0)
92
+ : 0;
93
+ const currCache = msg.usage.cacheRead ?? 0;
94
+
95
+ return currCache > prevCache;
96
+ })
97
+ .map((msg) => {
98
+ return {
99
+ timestamp: msg.timestamp,
100
+ responseId: msg.responseId,
101
+ usage: msg.usage,
102
+ };
103
+ });
104
+ ```
@@ -0,0 +1,10 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from "@earendil-works/pi-coding-agent";
5
+
6
+ export default function (pi: ExtensionAPI, getCtx: () => ExtensionContext) {
7
+ // Your code here
8
+ // return getCtx().getSystemPrompt();
9
+ return pi.getActiveTools();
10
+ }
@@ -0,0 +1,166 @@
1
+ // src/index.ts
2
+ import { existsSync, readFileSync, writeFileSync } from "fs";
3
+ import { mkdtemp, writeFile } from "fs/promises";
4
+ import { tmpdir } from "os";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ import {
8
+ DEFAULT_MAX_BYTES,
9
+ DEFAULT_MAX_LINES,
10
+ formatSize,
11
+ truncateHead,
12
+ withFileMutationQueue
13
+ } from "@earendil-works/pi-coding-agent";
14
+ import { createJiti } from "jiti";
15
+ import { Type } from "typebox";
16
+ function templateFilePath() {
17
+ const dir = path.dirname(fileURLToPath(import.meta.url));
18
+ const beside = path.join(dir, "TEMPLATE.ts");
19
+ if (existsSync(beside)) return beside;
20
+ return path.join(dir, "../static/TEMPLATE.ts");
21
+ }
22
+ function readCommandSource(fullPath) {
23
+ const existing = existsSync(fullPath) ? readFileSync(fullPath, "utf8") : "";
24
+ if (existing.trim()) return existing;
25
+ writeFileSync(fullPath, readFileSync(templateFilePath()));
26
+ return void 0;
27
+ }
28
+ async function invokeDefaultExport(mod, pi, getCtx) {
29
+ const exported = mod?.default ?? mod;
30
+ if (typeof exported !== "function") {
31
+ throw new Error("No default export function found");
32
+ }
33
+ return await exported(pi, getCtx);
34
+ }
35
+ async function runDefaultExport(load, pi, getCtx) {
36
+ try {
37
+ const value = await invokeDefaultExport(await load(), pi, getCtx);
38
+ const text = value === void 0 ? "Done (no return value)" : typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
39
+ return { ok: true, value, text };
40
+ } catch (error) {
41
+ return {
42
+ ok: false,
43
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
44
+ };
45
+ }
46
+ }
47
+ function toolDescription(template) {
48
+ return [
49
+ "Evaluates a TypeScript module on this session's jiti instance and returns what its default export returns.",
50
+ "The default export is called as (pi, getCtx) => result. pi is the agent ExtensionAPI. getCtx() returns the current ExtensionContext. Listeners registered on pi remain after this call. The module runs in the agent process with this extension's privileges. There is no sandbox.",
51
+ "Imports resolve from the session working directory and are read again on each call. import.meta.url is not a file on disk.",
52
+ 'An object or array result is JSON. undefined is the text "Done (no return value)". Any other result is String(value). A thrown error, or a module with no default-export function, is an error result whose text starts with "Error: ".',
53
+ `Output is truncated to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)}, whichever is hit first. The notice names the temp file that holds the full text.`,
54
+ "Module shape:",
55
+ template.trimEnd()
56
+ ].join("\n\n");
57
+ }
58
+ async function present(text) {
59
+ const truncation = truncateHead(text);
60
+ if (!truncation.truncated) {
61
+ return { text, details: { truncated: false } };
62
+ }
63
+ const dir = await mkdtemp(path.join(tmpdir(), "pi-debug-"));
64
+ const fullOutputPath = path.join(dir, "output.txt");
65
+ await withFileMutationQueue(fullOutputPath, async () => {
66
+ await writeFile(fullOutputPath, text, "utf8");
67
+ });
68
+ const notice = truncation.firstLineExceedsLimit ? `[Output truncated: first line exceeds ${formatSize(DEFAULT_MAX_BYTES)}. Full output saved to: ${fullOutputPath}]` : `
69
+
70
+ [Output truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). Full output saved to: ${fullOutputPath}]`;
71
+ const shown = truncation.firstLineExceedsLimit ? notice : truncation.content + notice;
72
+ return { text: shown, details: { truncated: true, fullOutputPath } };
73
+ }
74
+ async function toolResult(text, isError = false) {
75
+ const presented = await present(text);
76
+ return {
77
+ content: [{ type: "text", text: presented.text }],
78
+ details: presented.details,
79
+ ...isError ? { isError: true } : {}
80
+ };
81
+ }
82
+ function index_default(pi) {
83
+ let lastCtx = null;
84
+ let jiti;
85
+ const getJiti = () => {
86
+ jiti ||= createJiti(import.meta.url, {
87
+ fsCache: false,
88
+ moduleCache: false
89
+ });
90
+ return jiti;
91
+ };
92
+ const getCtx = () => lastCtx;
93
+ const loadSource = (code, cwd) => getJiti().evalModule(code, {
94
+ filename: path.join(cwd, ".pi-debug-eval.ts"),
95
+ async: true
96
+ });
97
+ for (const event of ["session_start", "turn_start"]) {
98
+ pi.on(event, (_event, ctx) => {
99
+ lastCtx = ctx;
100
+ });
101
+ }
102
+ pi.registerCommand("pi-debug", {
103
+ description: "Run a .ts file with default export function",
104
+ handler: async (args, ctx) => {
105
+ lastCtx = ctx;
106
+ let filePath = args?.trim() || "";
107
+ if (filePath.startsWith("@")) filePath = filePath.slice(1);
108
+ if (!filePath) {
109
+ ctx.ui.notify("Usage: /pi-debug <file-path>", "info");
110
+ return;
111
+ }
112
+ const fullPath = path.resolve(ctx.cwd, filePath);
113
+ const code = readCommandSource(fullPath);
114
+ if (code === void 0) {
115
+ ctx.ui.notify(`${filePath} updated with a code template`);
116
+ return;
117
+ }
118
+ const run = await runDefaultExport(
119
+ () => loadSource(code, ctx.cwd),
120
+ pi,
121
+ getCtx
122
+ );
123
+ if (!run.ok) {
124
+ ctx.ui.notify(run.text, "error");
125
+ return;
126
+ }
127
+ ctx.ui.notify(
128
+ run.value === void 0 ? run.text : `Result:
129
+ ${run.text}`,
130
+ "info"
131
+ );
132
+ }
133
+ });
134
+ pi.registerTool({
135
+ name: "pi-debug",
136
+ label: "Pi Debug",
137
+ description: toolDescription(readFileSync(templateFilePath(), "utf8")),
138
+ promptSnippet: "Run TypeScript inside the live Pi agent",
139
+ promptGuidelines: [
140
+ "Use pi-debug when the answer depends on live agent state, such as the system prompt, active tools, or session entries, instead of inferring that state from files.",
141
+ "Do not use bash or a subprocess instead of pi-debug for live agent state; a subprocess cannot see this process.",
142
+ "After pi-debug returns, use that result, and call pi-debug again only if agent state may have changed."
143
+ ],
144
+ parameters: Type.Object({
145
+ code: Type.String({
146
+ description: "TypeScript module source to evaluate. Include imports and the default export; not a function body."
147
+ })
148
+ }),
149
+ executionMode: "sequential",
150
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
151
+ if (signal?.aborted) return toolResult("Cancelled", true);
152
+ lastCtx = ctx;
153
+ if (!params.code.trim())
154
+ return toolResult("Error: No code provided", true);
155
+ const run = await runDefaultExport(
156
+ () => loadSource(params.code, ctx.cwd),
157
+ pi,
158
+ getCtx
159
+ );
160
+ return toolResult(run.text, !run.ok);
161
+ }
162
+ });
163
+ }
164
+ export {
165
+ index_default as default
166
+ };
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@vamsitalupula/pi-debug",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "repository": "github:tanavamsikrishna/pi-debug",
4
+ "version": "2.0.0",
5
+ "description": "Run Typescript within the Node.js context of the Pi coding agent",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "tsup src/index.ts --format esm --clean --outDir extensions --publicDir ./static/ --external @earendil-works/pi-coding-agent",
9
+ "dev": "tsup src/index.ts --format esm --watch --outDir extensions --publicDir ./static/ --external @earendil-works/pi-coding-agent",
10
+ "test": "node --experimental-strip-types --test test/pi-debug.test.ts"
11
+ },
12
+ "files": [
13
+ "extensions",
14
+ "package.json"
15
+ ],
16
+ "main": "extensions/index.js",
17
+ "keywords": [
18
+ "pi",
19
+ "pi-package",
20
+ "extension",
21
+ "typescript",
22
+ "jiti",
23
+ "devtool",
24
+ "runner"
25
+ ],
26
+ "author": "",
27
+ "license": "ISC",
28
+ "dependencies": {
29
+ "jiti": "^2.7.0",
30
+ "typebox": "^1.1.24"
31
+ },
32
+ "devDependencies": {
33
+ "@earendil-works/pi-coding-agent": "^0.74.2",
34
+ "tsup": "^8.5.1",
35
+ "typescript": "^5.9.3"
36
+ },
37
+ "access": "public",
38
+ "pi": {
39
+ "extensions": [
40
+ "extensions/"
41
+ ],
42
+ "image": "https://raw.githubusercontent.com/tanavamsikrishna/pi-debug/refs/heads/main/screenshot.png"
43
+ }
44
+ }