pi-petrroll 0.1.1 → 0.2.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/README.md +31 -6
- package/extensions/cd/index.ts +22 -5
- package/extensions/cost/index.ts +52 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,12 +5,13 @@ Small, focused extensions for [Pi](https://pi.dev). MIT licensed.
|
|
|
5
5
|
| Extension | Command | Agent tool |
|
|
6
6
|
| --- | --- | --- |
|
|
7
7
|
| [Change directory](#change-directory) | `/cd <path>` | `change_directory` |
|
|
8
|
+
| [External costs](#external-costs) | — | `add_cost` |
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
Independent, focused extensions. No skills, shell wrappers, or replacement built-in tools.
|
|
10
11
|
|
|
11
12
|
## Install
|
|
12
13
|
|
|
13
|
-
Requires **
|
|
14
|
+
Requires **Pi 0.85.1 or newer** (`@earendil-works/pi-coding-agent`); tested against 0.85.1 and 1.1.0. Change directory requires interactive mode; external cost recording works in all modes. Earlier Pi versions may not rebuild cwd-bound services when switching sessions.
|
|
14
15
|
|
|
15
16
|
```sh
|
|
16
17
|
pi install npm:pi-petrroll
|
|
@@ -46,7 +47,7 @@ All extensions load by default. To load only `cd`, including after future update
|
|
|
46
47
|
}
|
|
47
48
|
```
|
|
48
49
|
|
|
49
|
-
Disabling `cd` disables both `/cd` and its `change_directory` tool.
|
|
50
|
+
Disabling `cd` disables both `/cd` and its `change_directory` tool. Disabling `cost` disables `add_cost`. To load only cost recording, use `"extensions": ["extensions/cost/index.ts"]` in the package entry above.
|
|
50
51
|
|
|
51
52
|
## Change directory
|
|
52
53
|
|
|
@@ -77,7 +78,7 @@ No quit, terminal `cd`, restart, or cross-directory session-copy confirmation. *
|
|
|
77
78
|
|
|
78
79
|
Bare `/cd` shows the current directory and usage. Relative paths use Pi's current cwd. `~`, `~/…`, single-quoted paths, JSON double-quoted paths, and symlinks are supported. Paths are not shell commands: no variable expansion, globbing, `~username`, or `cd -`.
|
|
79
80
|
|
|
80
|
-
Manual `/cd` switches without starting another model response; the agent tool resumes automatically
|
|
81
|
+
Manual `/cd` switches without starting another model response; the agent tool resumes automatically **after session replacement has fully completed**. The tool intentionally ends the old run, but normally needs no manual `continue`. An existing destination is required. The extension does not create directories or worktrees itself.
|
|
81
82
|
|
|
82
83
|
### What changes
|
|
83
84
|
|
|
@@ -97,9 +98,31 @@ Manual `/cd` switches without starting another model response; the agent tool re
|
|
|
97
98
|
- The agent should call `change_directory` **alone**, after worktree creation has completed. The tool requests early termination and the command waits for idle so completed tool results are included in the copy. If mixed with other tools, those calls still run in the **old** directory; the switch waits until the run settles.
|
|
98
99
|
- Explicitly aborting a pending tool handoff prevents the switch. Invalid paths and concurrent requests are rejected.
|
|
99
100
|
- A cancelled session switch removes the unused copy and keeps the original session. If Pi fails while rebuilding the replacement runtime, its normal fatal-error handling applies; the original and any created copy remain available for recovery.
|
|
101
|
+
- If submitting the automatic continuation fails, the extension warns without treating the successful switch as a failure. Model errors and user aborts still follow Pi's normal behavior; the extension does not retry aborted runs. Send `continue` in the destination if needed.
|
|
100
102
|
- The copied history can contain references to the old project. A visible context message records the change, and the new system prompt contains the destination's instructions.
|
|
101
103
|
- Switching reloads project extensions according to Pi's trust policy. Install only code you trust.
|
|
102
104
|
|
|
105
|
+
## External costs
|
|
106
|
+
|
|
107
|
+
After launching a subagent with the ordinary `bash` tool, Pi can call `add_cost` to include the subagent's reported cost in the **current session's native cost total**:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"amount": 0.0375,
|
|
112
|
+
"reason": "Bash-launched review subagent, run review-123"
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- `amount` is an additional, finite, non-negative cost in **USD**, not cents. Zero is allowed.
|
|
117
|
+
- `reason` is a required, non-blank description (up to 1000 characters), saved with the tool result for an audit trail.
|
|
118
|
+
- Costs appear in Pi's footer, `/session`, and RPC session statistics. Pi stores them as tool-result usage, so they survive reload/resume and compaction without inventing token counts or changing the parent model's usage.
|
|
119
|
+
- Works in interactive, print, JSON, and RPC modes, including in-memory sessions. `--no-session` costs exist only for that process.
|
|
120
|
+
- This is accounting only: the tool does not launch anything, inspect subagent output, estimate pricing, or charge money. The agent must obtain the cost from the external run first.
|
|
121
|
+
|
|
122
|
+
**Every successful call is additive; there is no deduplication.** Report each external cost once. For a continued subagent, report only the new, unrecorded cost—not its cumulative lifetime total. Do not report parent-session model costs or nested usage already returned by another tool. If the external cost is unknown, do not guess.
|
|
123
|
+
|
|
124
|
+
Pi's totals include all entries in the session file, including abandoned branches. Forked or copied sessions inherit costs present in their copied history; a new empty session starts at zero. No session files or Pi internals are patched.
|
|
125
|
+
|
|
103
126
|
## Development
|
|
104
127
|
|
|
105
128
|
```sh
|
|
@@ -108,15 +131,17 @@ npm run check
|
|
|
108
131
|
npm test
|
|
109
132
|
```
|
|
110
133
|
|
|
111
|
-
Tests cover path handling, copy/branch preservation, cancellation, aborts, concurrency, and a real Pi runtime handoff with a fake model stream. The runtime test verifies that tool results are copied, `read` and `bash` use the new cwd, destination `AGENTS.md` replaces source context, and the agent continues. Tests do not call a model API or use your credentials.
|
|
134
|
+
Tests cover cost validation, native cost accumulation, persistence/resume, compaction, path handling, copy/branch preservation, cancellation, aborts, concurrency, and a real Pi runtime handoff with a fake model stream. The runtime test verifies that tool results are copied, `read` and `bash` use the new cwd, destination `AGENTS.md` replaces source context, and the agent continues only after the host finishes switching. Unit tests also cover continuation-submission failures without rolling back the successful switch. Tests do not call a model API or use your credentials.
|
|
112
135
|
|
|
113
136
|
```text
|
|
114
137
|
extensions/
|
|
115
138
|
cd/
|
|
116
139
|
index.ts
|
|
117
|
-
|
|
140
|
+
cost/
|
|
141
|
+
index.ts
|
|
118
142
|
tests/
|
|
119
143
|
cd.test.ts
|
|
144
|
+
cost.test.ts
|
|
120
145
|
runtime.test.ts
|
|
121
146
|
```
|
|
122
147
|
|
package/extensions/cd/index.ts
CHANGED
|
@@ -80,15 +80,27 @@ export default function cdExtension(pi: ExtensionAPI) {
|
|
|
80
80
|
{ from: ctx.cwd, to: target, parentSession: source },
|
|
81
81
|
);
|
|
82
82
|
const destination = copy.getSessionFile()!;
|
|
83
|
-
|
|
83
|
+
let resume: (() => Promise<void>) | undefined;
|
|
84
84
|
const result = await ctx.switchSession(destination, {
|
|
85
85
|
withSession: async (fresh) => {
|
|
86
86
|
fresh.ui.notify(`Changed directory to ${fresh.cwd}`, "info");
|
|
87
87
|
if (request) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
// Capture only the fresh context, not the invalidated pi/ctx.
|
|
89
|
+
resume = async () => {
|
|
90
|
+
try {
|
|
91
|
+
await fresh.sendUserMessage(
|
|
92
|
+
"The requested directory change has completed. Continue the outstanding task in this directory. " +
|
|
93
|
+
"If no work remains, briefly confirm the change.",
|
|
94
|
+
{ deliverAs: "followUp" },
|
|
95
|
+
);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
fresh.ui.notify(
|
|
98
|
+
`Directory changed, but automatic continuation failed: ${error instanceof Error ? error.message : String(error)}. ` +
|
|
99
|
+
'Send "continue" to resume.',
|
|
100
|
+
"warning",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
92
104
|
}
|
|
93
105
|
},
|
|
94
106
|
});
|
|
@@ -100,7 +112,12 @@ export default function cdExtension(pi: ExtensionAPI) {
|
|
|
100
112
|
display: true,
|
|
101
113
|
});
|
|
102
114
|
ctx.ui.notify("Directory change cancelled.", "warning");
|
|
115
|
+
return;
|
|
103
116
|
}
|
|
117
|
+
// sendUserMessage awaits the entire agent run. Do not run it inside
|
|
118
|
+
// withSession: the host must finish switching first, and prompt errors
|
|
119
|
+
// must not be treated as fatal failures to replace the runtime.
|
|
120
|
+
await resume?.();
|
|
104
121
|
} finally {
|
|
105
122
|
// Local state only; session-bound pi/ctx may now be stale.
|
|
106
123
|
busy = false;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Type, type Static } from "typebox";
|
|
3
|
+
|
|
4
|
+
export const addCostParameters = Type.Object({
|
|
5
|
+
amount: Type.Number({ minimum: 0, description: "Additional cost in USD (not cents or a cumulative session total)." }),
|
|
6
|
+
reason: Type.String({ minLength: 1, maxLength: 1000, description: "What incurred this cost, e.g. the bash-launched subagent task or run ID." }),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export type AddCostInput = Static<typeof addCostParameters>;
|
|
10
|
+
|
|
11
|
+
export default function (pi: ExtensionAPI) {
|
|
12
|
+
pi.registerTool({
|
|
13
|
+
name: "add_cost",
|
|
14
|
+
label: "Add Cost",
|
|
15
|
+
description:
|
|
16
|
+
"Add an external cost in USD to the current Pi session's native cost total. " +
|
|
17
|
+
"Use after external work such as a subagent launched via bash reports its cost. " +
|
|
18
|
+
"Every successful call adds the amount again; report only the unrecorded cost, never a cumulative total or costs Pi already accounts for. " +
|
|
19
|
+
"This records accounting only: it does not run a subagent or charge money.",
|
|
20
|
+
promptSnippet: "Record external costs in the current session total",
|
|
21
|
+
promptGuidelines: [
|
|
22
|
+
"Use add_cost after bash-launched subagents or other external work report a cost in USD. Do not guess costs or double-count costs already recorded by Pi or a previous add_cost call.",
|
|
23
|
+
],
|
|
24
|
+
parameters: addCostParameters,
|
|
25
|
+
async execute(_toolCallId, { amount, reason }, signal) {
|
|
26
|
+
signal?.throwIfAborted();
|
|
27
|
+
// Validate here too: tool_call hooks can mutate arguments after schema validation.
|
|
28
|
+
if (typeof amount !== "number" || !Number.isFinite(amount) || amount < 0) {
|
|
29
|
+
throw new Error("amount must be a finite, non-negative cost in USD.");
|
|
30
|
+
}
|
|
31
|
+
if (typeof reason !== "string" || !reason.trim() || reason.length > 1000) {
|
|
32
|
+
throw new Error("reason must be non-blank and at most 1000 characters.");
|
|
33
|
+
}
|
|
34
|
+
const description = reason.trim();
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: "text", text: `Added $${amount} USD to this session's cost: ${description}` }],
|
|
37
|
+
details: { amount, currency: "USD", reason: description },
|
|
38
|
+
// Pi persists tool-result usage and includes it in footer, /session, and
|
|
39
|
+
// RPC totals, including after compaction. No fabricated token counts or
|
|
40
|
+
// attribution to the parent model's input/output pricing categories.
|
|
41
|
+
usage: {
|
|
42
|
+
input: 0,
|
|
43
|
+
output: 0,
|
|
44
|
+
cacheRead: 0,
|
|
45
|
+
cacheWrite: 0,
|
|
46
|
+
totalTokens: 0,
|
|
47
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: amount },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-petrroll",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Small, focused Pi extensions by petrroll:
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Small, focused Pi extensions by petrroll: switch projects and track external costs in your session.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Petr Houška (petrroll)",
|