localpi 0.5.2 → 0.6.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 +362 -10
- package/dist/src/cli/cli.js +112 -5
- package/dist/src/llm/openai.js +65 -4
- package/dist/src/localpi/acp.js +118 -0
- package/dist/src/localpi/catalog.js +79 -7
- package/dist/src/localpi/catppuccin.js +64 -0
- package/dist/src/localpi/llama-server.js +72 -39
- package/dist/src/localpi/model-profile.js +4 -0
- package/dist/src/localpi/options.js +129 -9
- package/dist/src/localpi/provider-registry.js +51 -3
- package/dist/src/localpi/runtime-connection.js +11 -8
- package/dist/src/localpi/runtime.js +12 -7
- package/dist/src/localpi/settings-state.js +13 -3
- package/dist/src/pi/app.js +11 -5
- package/dist/src/pi/extension-sources/continue-on-truncation.js +55 -0
- package/dist/src/pi/extension-sources/settings-file.js +31 -0
- package/dist/src/pi/extension-sources/status-line.js +424 -0
- package/dist/src/pi/extension-sources/thinking-control.js +4 -47
- package/dist/src/pi/extension-sources/token-status.js +544 -119
- package/dist/src/pi/extension-sources/tool-approval.js +155 -14
- package/dist/src/pi/extensions.js +37 -10
- package/dist/src/pi/skills.js +24 -0
- package/dist/src/pi/theme.js +107 -0
- package/docs/2026-06-16-startup-model-and-thinking-control-plan.md +39 -11
- package/docs/2026-09-23-acp-mode-plan.md +111 -0
- package/docs/2026-09-24-continue-on-truncation-plan.md +115 -0
- package/docs/design-principles.md +114 -0
- package/docs/implementation-plan.md +33 -0
- package/docs/runtime-specification.md +98 -4
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
# localpi
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/cover.svg" alt="localpi: an unopinionated Pi distribution that makes it easy to test and work with small local models on constrained systems" width="880">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
Localpi is an unopinionated Pi distribution that makes it easy to test and work with small local models on constrained systems.
|
|
4
8
|
|
|
5
9
|
By default, Localpi discovers available local providers, lets you choose when more than one model is loaded, points Pi at the selected model, and writes Pi config for the other discovered models so `/model` can switch among them during the session.
|
|
6
10
|
|
|
7
|
-
Localpi is meant to be the practical bridge from Pi to local inference stacks such as llama.cpp/`llama-server`, vLLM, SGLang, LM Studio, Ollama, and custom provider endpoints.
|
|
11
|
+
Localpi is meant to be the practical bridge from Pi to local inference stacks such as llama.cpp/`llama-server`, vLLM, SGLang, LM Studio, Ollama, and custom provider endpoints. llama.cpp is the default engine: Localpi probes a running llama.cpp server first and prefers its loaded models.
|
|
8
12
|
|
|
9
13
|
Localpi is intentionally generic. It does not contain classifier prompts, dataset workflows, GitHub routing logic, or final-schema output machinery. Structured classifier runs belong in caller tools such as `localpager-agent`.
|
|
10
14
|
|
|
15
|
+
A Localpi session keeps its context light. Localpi appends three sentences to Pi's own system prompt, adds no datasets, prompt packs, or memory files, and loads only its own skills directory, so a session starts with about 2.9k tokens of baseline context on the default tool set and a small context window still has room for real work.
|
|
16
|
+
|
|
11
17
|
See:
|
|
12
18
|
|
|
13
19
|
- [Runtime Specification](docs/runtime-specification.md)
|
|
20
|
+
- [Design Principles](docs/design-principles.md)
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- Node.js 22.19.0 or newer, which Pi requires.
|
|
25
|
+
- A running inference server: llama.cpp/`llama-server`, LM Studio, vLLM, SGLang, or any OpenAI-compatible endpoint. Localpi never starts, stops, or unloads a server it did not start itself.
|
|
26
|
+
- A terminal that Pi supports.
|
|
14
27
|
|
|
15
28
|
## Install
|
|
16
29
|
|
|
@@ -36,6 +49,22 @@ After build:
|
|
|
36
49
|
node dist/src/cli/main.js --status
|
|
37
50
|
```
|
|
38
51
|
|
|
52
|
+
## Pi and pi-factory Versions
|
|
53
|
+
|
|
54
|
+
Localpi launches Pi through `npx -y @earendil-works/pi-coding-agent@latest`, so a normal launch
|
|
55
|
+
runs the newest Pi release. Point it somewhere else with `--pi-command`, or `LOCALPI_PI_CMD`, as a
|
|
56
|
+
program plus arguments:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
localpi --pi-command "node /opt/pi/bin/pi"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Localpi builds its Pi launch on [pi-factory](https://github.com/osolmaz/pi-factory) and keeps that
|
|
63
|
+
dependency current. The `@earendil-works/pi-coding-agent` devDependency pins the Pi release that
|
|
64
|
+
localpi is written against, and `tests/generated-extension-types.test.ts` typechecks every
|
|
65
|
+
generated extension against those types, so Pi extension API drift fails `npm test` instead of
|
|
66
|
+
failing at launch.
|
|
67
|
+
|
|
39
68
|
## Runtime Model
|
|
40
69
|
|
|
41
70
|
Target default:
|
|
@@ -72,14 +101,38 @@ Localpi launches Pi with:
|
|
|
72
101
|
|
|
73
102
|
- default tools: `read,bash,edit,write,grep,find,ls`
|
|
74
103
|
- a system prompt that explains local tool approval and local-model limits
|
|
75
|
-
- an approval gate before every tool call
|
|
76
|
-
- token speed and
|
|
104
|
+
- an approval gate before every tool call, which you can turn off for the session with `/approval`
|
|
105
|
+
- token speed, prefill progress, and context usage while responses stream
|
|
106
|
+
- a Catppuccin Mocha theme for the Pi session, written into `<state-dir>/pi-themes/`
|
|
107
|
+
- skills from `<state-dir>/pi-skills/` only, so shared skill directories stay out of the session
|
|
77
108
|
- bounded Gemma/llama-server reasoning controlled by `--thinking`
|
|
78
|
-
-
|
|
109
|
+
- in-session `/thinking` (Pi's own command) and `/approval` (localpi's) for changing session settings
|
|
79
110
|
- local state under `~/.local/state/localpi`
|
|
80
111
|
|
|
81
112
|
The approval gate makes failed or denied tool calls explicit to the model so the model does not claim that a blocked command ran.
|
|
82
113
|
|
|
114
|
+
## Skills
|
|
115
|
+
|
|
116
|
+
Pi discovers skills from shared directories such as `~/.agents/skills` and `~/.pi/agent/skills`. A
|
|
117
|
+
localpi session does not use those. It launches Pi with `--no-skills` and loads only
|
|
118
|
+
`<state-dir>/pi-skills/`, so a small local model does not carry skill lists it cannot use.
|
|
119
|
+
|
|
120
|
+
Put your own localpi skills there, one directory with a `SKILL.md` per skill:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
mkdir -p ~/.local/state/localpi/pi-skills/my-skill
|
|
124
|
+
$EDITOR ~/.local/state/localpi/pi-skills/my-skill/SKILL.md
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Pick the source with `--skills` or `LOCALPI_SKILLS`:
|
|
128
|
+
|
|
129
|
+
- `own` (default): `--no-skills` plus `<state-dir>/pi-skills/`
|
|
130
|
+
- `ambient`: Pi's normal discovery, including `~/.agents/skills` and project `.agents/skills`
|
|
131
|
+
- `off`: `--no-skills` and nothing else
|
|
132
|
+
|
|
133
|
+
An explicit `--skill <path>` still works in every mode, because Pi loads explicit paths even with
|
|
134
|
+
`--no-skills`.
|
|
135
|
+
|
|
83
136
|
## Diffusion Canvas Visualizer
|
|
84
137
|
|
|
85
138
|
The live diffusion canvas visualizer (watching DiffusionGemma denoise its
|
|
@@ -88,6 +141,265 @@ answer in the TUI) lives in its own project now:
|
|
|
88
141
|
bundle plus a standalone Pi widget package; the widget also installs into any
|
|
89
142
|
Pi session via `pi install`.
|
|
90
143
|
|
|
144
|
+
## Tool Approval
|
|
145
|
+
|
|
146
|
+
Approval is on by default. Every tool call opens a dialog that shows the tool name and its input,
|
|
147
|
+
with three choices:
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
Allow once # run this call and keep asking for the next one
|
|
151
|
+
Allow all tools for this session # run every tool call in this session without asking
|
|
152
|
+
Deny and stop # block the call and end the turn
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A denied call does not run, the model is told that the call was blocked, and the turn stops instead
|
|
156
|
+
of letting the model try another way. Pressing escape in the dialog counts as a deny, so one key
|
|
157
|
+
ends the turn. Pi stops the turn when every blocked result in the current tool batch asks for it,
|
|
158
|
+
so a batch that also contains an allowed read-only call finishes that call first.
|
|
159
|
+
|
|
160
|
+
Read-only tools (`read`, `grep`, `find`, `ls`) run without a dialog, because they cannot change the
|
|
161
|
+
workspace. `bash` still asks, because a bash command can write. An unknown tool also asks, because
|
|
162
|
+
localpi cannot know what it does. Use `--approve-read-tools`, or `LOCALPI_APPROVE_READ_TOOLS=1`, to
|
|
163
|
+
put read-only tools behind the gate too.
|
|
164
|
+
|
|
165
|
+
The dialog choice lasts for this session only. Use the permission setting to change what new
|
|
166
|
+
sessions do:
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
/approval allow # do not ask in new sessions either
|
|
170
|
+
/approval ask # ask before each tool call (the default)
|
|
171
|
+
/approval off # alias for /approval allow
|
|
172
|
+
/approval on # alias for /approval ask
|
|
173
|
+
/approval # pick ask or allow from a list, with the current value marked
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The permission setting lives in `<state-dir>/settings.json` as `"permission": "ask" | "allow"`.
|
|
177
|
+
Startup reads, in order: `--no-approval` or `LOCALPI_APPROVAL=0`, the saved setting, then the
|
|
178
|
+
default `ask`. Use `--no-approval` to start one session without changing the setting.
|
|
179
|
+
|
|
180
|
+
While approval is off, Pi shows `permission: allow` in the status area, so the state stays visible.
|
|
181
|
+
|
|
182
|
+
In a non-interactive launch, approval stays on and no dialog is possible, so every tool call is
|
|
183
|
+
blocked. That keeps scripted runs from executing tools without a person watching.
|
|
184
|
+
|
|
185
|
+
## ACP Mode
|
|
186
|
+
|
|
187
|
+
`--acp` and `LOCALPI_ACP=1` serve the Agent Client Protocol on stdio, so an ACP client such as an
|
|
188
|
+
editor can drive the same Pi session that a normal launch runs.
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
localpi --acp --model gemma-e4b
|
|
192
|
+
LOCALPI_ACP=1 localpi --model gemma-e4b
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
ACP mode:
|
|
196
|
+
|
|
197
|
+
- resolves the runtime and writes the same Pi configuration a normal launch writes, including
|
|
198
|
+
`<state-dir>/pi-config-runtime/models.json` and `settings.json`
|
|
199
|
+
- starts the pinned `pi-acp` adapter from `node_modules` as a child process with inherited stdio
|
|
200
|
+
- writes a launcher script to `<state-dir>/acp/pi-launcher.sh` and points `PI_ACP_PI_COMMAND` at it,
|
|
201
|
+
because the adapter starts Pi itself and passes only its own arguments. The script quotes the Pi
|
|
202
|
+
program and every argument, so a path with a space survives, and execs Pi with the full launch line
|
|
203
|
+
a normal launch uses, so the extensions, the system prompt, the theme, and the tool flags stay the
|
|
204
|
+
same
|
|
205
|
+
- passes the environment a normal launch uses, and sets `LOCALPI_ACP=0` for the child
|
|
206
|
+
- requires an explicit `--model` or `LOCALPI_MODEL`, because there is no terminal for the startup
|
|
207
|
+
model picker
|
|
208
|
+
- keeps stdout for protocol bytes only, and writes diagnostics and warnings to stderr
|
|
209
|
+
- refuses `--demo`, a forwarded Pi `--mode`, Pi session flags, and forwarded prompts, because the
|
|
210
|
+
adapter owns the session
|
|
211
|
+
- refuses `--status`, `--stop`, and `--list` together with `--acp` on the command line, and lets a
|
|
212
|
+
command-line immediate command win over an environment-set `LOCALPI_ACP=1`, as `LOCALPI_DEMO` does
|
|
213
|
+
- refuses to start localpi as the adapter's Pi command, so a child cannot re-enter ACP mode
|
|
214
|
+
|
|
215
|
+
Approval still works: the adapter forwards Pi's extension dialogs to the ACP client, so the client
|
|
216
|
+
asks before a tool call runs. The adapter does not pass `--no-extensions`, so Pi extension discovery
|
|
217
|
+
stays on.
|
|
218
|
+
|
|
219
|
+
Pi has no ACP mode of its own. ACP support comes from `pi-acp` (MIT), which `package.json` pins to
|
|
220
|
+
an exact version, and localpi never vendors its source. Set `LOCALPI_ACP_ADAPTER` to a path to run a
|
|
221
|
+
different adapter build.
|
|
222
|
+
|
|
223
|
+
## Continue On Truncation
|
|
224
|
+
|
|
225
|
+
`--continue-on-truncation <n>` and `LOCALPI_CONTINUE_ON_TRUNCATION=<n>` continue a reply that the
|
|
226
|
+
model cut off at the declared output limit, so a run finishes its answer instead of ending with a
|
|
227
|
+
half-written one.
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
localpi --model gemma-e4b --max-tokens 4096 --continue-on-truncation 2
|
|
231
|
+
LOCALPI_CONTINUE_ON_TRUNCATION=2 localpi --model gemma-e4b
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`n` is the maximum number of extra continuations. Pi reports a turn that reached the output cap as a
|
|
235
|
+
length stop, and localpi sends one follow-up message that asks the model to continue where it
|
|
236
|
+
stopped without repeating earlier text. The guard:
|
|
237
|
+
|
|
238
|
+
- is off by default. With no flag and no environment variable, a normal launch, an ACP launch, and
|
|
239
|
+
demo mode behave exactly as they do today
|
|
240
|
+
- takes the flag first, then `LOCALPI_CONTINUE_ON_TRUNCATION`, then off
|
|
241
|
+
- treats `LOCALPI_CONTINUE_ON_TRUNCATION=0` as off, so an inherited value can be disabled without
|
|
242
|
+
dropping the variable, and rejects any other value that is not a positive integer with exit code 2
|
|
243
|
+
- reacts only to the length stop, never to a normal stop, an error, an abort, or a tool call
|
|
244
|
+
- leaves a truncated turn that already asked for a tool alone, because Pi runs the tool and keeps
|
|
245
|
+
going on its own
|
|
246
|
+
- counts continuations per session, stops at the limit, and prints one line to stderr when the reply
|
|
247
|
+
was still cut off
|
|
248
|
+
- writes its notes to stderr only, so stdout stays free for a batch run
|
|
249
|
+
- changes no served limit. `--max-tokens` and `--context-window` keep their meaning, and the guard
|
|
250
|
+
only reacts to the stop reason Pi reports
|
|
251
|
+
|
|
252
|
+
The guard installs as the `continue-on-truncation.ts` Pi extension. It is not a default extension,
|
|
253
|
+
so a normal session writes it only when you ask for it. It works in ACP mode too, because both
|
|
254
|
+
launch paths share the same Pi configuration and extension bundle.
|
|
255
|
+
|
|
256
|
+
## llama.cpp (Default Engine)
|
|
257
|
+
|
|
258
|
+
llama.cpp is Localpi's default local engine.
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
localpi
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
With the default `auto` runtime, Localpi probes a running llama.cpp server at `http://127.0.0.1:8080/v1` first. A loaded llama.cpp model wins automatic selection ahead of LM Studio, vLLM, and the managed `llama-server` fallback. Localpi reads llama.cpp `/v1/models` status: a model is usable when it is loaded or the server does not report status, and an unloaded model is offered only when `/props` reports `models_autoload`. Localpi never starts, stops, or unloads an external llama.cpp server.
|
|
265
|
+
|
|
266
|
+
Run explicitly against llama.cpp:
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
localpi --runtime llama-cpp
|
|
270
|
+
localpi --runtime llama-cpp --base-url http://127.0.0.1:8080/v1 --model ternary-bonsai-2-27b-pq2_0
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Point at a llama.cpp server on another port with a provider registry entry:
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"providers": {
|
|
278
|
+
"llama-cpp": {
|
|
279
|
+
"type": "llama-cpp",
|
|
280
|
+
"baseUrl": "http://127.0.0.1:9931/v1",
|
|
281
|
+
"discover": true
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Image input
|
|
288
|
+
|
|
289
|
+
When a llama.cpp model takes images, Localpi says so in the Pi model config, so the `read` tool and `@file` attachments can send a picture to that model. Localpi reads the fact from the server: an entry in `/v1/models` that lists `image` in `architecture.input_modalities` gets `input: ["text", "image"]`. Every other model stays text-only. No model name is used to guess this.
|
|
290
|
+
|
|
291
|
+
An engine that reports nothing can be described with a model profile:
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
{
|
|
295
|
+
"id": "qwen3-vl-8b",
|
|
296
|
+
"model": "qwen3-vl-8b",
|
|
297
|
+
"capabilities": { "image": true }
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
The profile wins over the server in both directions, so `"image": false` also turns image input off for a model that reports it.
|
|
302
|
+
|
|
303
|
+
The server must load a multimodal projector for the model, which a llama.cpp router does on its own from the model directory. Check it with one real image request before you rely on it.
|
|
304
|
+
|
|
305
|
+
## Status Display
|
|
306
|
+
|
|
307
|
+
The status display answers one question: is this machine keeping up? It shows elapsed time, output
|
|
308
|
+
tokens, token rate, and context use. Pick a mode with `--stats`:
|
|
309
|
+
|
|
310
|
+
| Mode | Status line | Live line | Transcript entry |
|
|
311
|
+
| ---------------- | ----------- | --------- | ---------------- |
|
|
312
|
+
| `off` | Pi's own | no | no |
|
|
313
|
+
| `line` | localpi | yes | no |
|
|
314
|
+
| `full` (default) | localpi | yes | yes |
|
|
315
|
+
|
|
316
|
+
The status line is one row. Localpi replaces Pi's two-row footer with one line that carries the same
|
|
317
|
+
facts plus the engine label next to the model:
|
|
318
|
+
|
|
319
|
+
```text
|
|
320
|
+
~/repos/localpi (main) · ↑3.2k ↓100 R3.2k CH99.5% · 43.8 tok/s · 9.8%/33k (llama.cpp) ternary-bonsai-2-27b-pq2_0
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
The line holds, from left to right:
|
|
324
|
+
|
|
325
|
+
- the working directory and the git branch,
|
|
326
|
+
- the token totals, the cache read, and the cache hit rate of the session,
|
|
327
|
+
- the token rate of the last finished turn,
|
|
328
|
+
- the context use as a percentage of the window,
|
|
329
|
+
- the engine that serves the model, then the model itself.
|
|
330
|
+
|
|
331
|
+
The engine label comes from the provider localpi built the catalog with, so it is read, not guessed. A
|
|
332
|
+
model from a provider with no known engine shows no label. When the model reports reasoning, the line
|
|
333
|
+
also shows the thinking level, the same way Pi does.
|
|
334
|
+
|
|
335
|
+
One number never appears twice at the same time. While the model runs, the live line owns the token
|
|
336
|
+
rate and the context use, and the status line shows the rest. When the session is idle, the rate of
|
|
337
|
+
the last finished turn and the context return to the status line, so the speed stays visible after
|
|
338
|
+
the answer ends. The status line drops the rate first when the row is too narrow.
|
|
339
|
+
|
|
340
|
+
The live line replaces Pi's plain `Working` text while the model runs:
|
|
341
|
+
|
|
342
|
+
```text
|
|
343
|
+
Working (1.8s · 100 out · 55.6 tok/s · ctx 34k/131k (26%))
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
During prefill, the same line reports progress through the prompt:
|
|
347
|
+
|
|
348
|
+
```text
|
|
349
|
+
Working (prefill 25% · 5k/20k tok · 3.2s · ctx 20k/33k (61%))
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Prefill progress needs a llama.cpp server, because it reads the server's `/slots` endpoint. Localpi
|
|
353
|
+
polls that endpoint only for llama.cpp runtimes. When the endpoint is missing or slow, localpi stops
|
|
354
|
+
polling and shows elapsed prefill time instead.
|
|
355
|
+
|
|
356
|
+
In `full` mode, each finished turn also adds one dim transcript line:
|
|
357
|
+
|
|
358
|
+
```text
|
|
359
|
+
10s · 438 out · 43.8 tok/s · 8.4k in · prefill 0.4s · ctx 34k/131k (26%)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Context colors use the active theme: normal below 80 percent, warning from 80 percent, error from
|
|
363
|
+
95 percent. When Pi reports no token counts, the live line falls back to the context percentage
|
|
364
|
+
alone.
|
|
365
|
+
|
|
366
|
+
Localpi shows the engine label in `line` and `full` mode. In `off` mode localpi leaves the footer and
|
|
367
|
+
the working line to Pi.
|
|
368
|
+
|
|
369
|
+
Change the mode during a session with `/stats`:
|
|
370
|
+
|
|
371
|
+
```text
|
|
372
|
+
/stats # pick a mode from a list
|
|
373
|
+
/stats line # or pass the mode directly
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
`/stats` saves the choice to `<state-dir>/settings.json`, so the next launch keeps it. Pass
|
|
377
|
+
`--stats <mode>` or set `LOCALPI_STATS` to override the saved value for one launch.
|
|
378
|
+
|
|
379
|
+
## Catppuccin Theme
|
|
380
|
+
|
|
381
|
+
Localpi gives each Pi session the Catppuccin Mocha palette. It writes its own copy of the theme to
|
|
382
|
+
`<state-dir>/pi-themes/catppuccin-mocha.json`, loads it into Pi, and selects it for the session. The
|
|
383
|
+
theme covers the full Pi TUI: messages, tool cards, diffs, syntax highlighting, and the localpi
|
|
384
|
+
status display.
|
|
385
|
+
|
|
386
|
+
Localpi writes its own copy for a good reason. A localpi session uses a Pi config directory inside
|
|
387
|
+
the localpi state directory, so Pi does not load the themes from your global Pi setup.
|
|
388
|
+
|
|
389
|
+
The theme belongs to the session only. Localpi never edits your global Pi themes or settings.
|
|
390
|
+
|
|
391
|
+
Localpi's own output uses the same palette: labels are `overlay1`, warnings are `peach`, and errors
|
|
392
|
+
are `red`. Colors are truecolor and appear only on a terminal. Piped output stays plain, and
|
|
393
|
+
`NO_COLOR` turns color off.
|
|
394
|
+
|
|
395
|
+
Escape hatches:
|
|
396
|
+
|
|
397
|
+
- `localpi --no-themes` starts the session with no theme at all.
|
|
398
|
+
- `localpi --use-theme onur-dark` selects your own theme instead. The Catppuccin file is still
|
|
399
|
+
loaded, so `/settings` can offer it.
|
|
400
|
+
- `localpi --theme <path>` adds another theme file, as Pi does in a normal session.
|
|
401
|
+
- `FORCE_COLOR=1` forces color in localpi's own output; `NO_COLOR=1` removes it.
|
|
402
|
+
|
|
91
403
|
## LM Studio Alternative
|
|
92
404
|
|
|
93
405
|
LM Studio exposes an OpenAI-compatible endpoint, usually:
|
|
@@ -149,7 +461,9 @@ Use a bounded reasoning budget with managed `llama-server`:
|
|
|
149
461
|
localpi --model gemma-12b --thinking low -p "classify this item"
|
|
150
462
|
```
|
|
151
463
|
|
|
152
|
-
In an interactive session, use `/thinking` to pick a level or
|
|
464
|
+
In an interactive session, use Pi's own `/thinking` command to pick a level or to set one directly. This changes Pi's active thinking level for later turns. Localpi remembers the level Pi selected and starts the next localpi launch from it. For managed `llama-server`, the server-side reasoning budget is still chosen at startup because changing it requires restarting the local server process.
|
|
465
|
+
|
|
466
|
+
Localpi does not register its own `/thinking` command, because Pi already owns that name.
|
|
153
467
|
|
|
154
468
|
For managed `llama-server`, thinking levels map to server-side reasoning:
|
|
155
469
|
|
|
@@ -202,11 +516,12 @@ demowall record --session demowall-<timestamp> --out demo.mp4 --seconds 60
|
|
|
202
516
|
|
|
203
517
|
## Options
|
|
204
518
|
|
|
205
|
-
- `--runtime <auto|llama-server|lmstudio|vllm|openai-compatible>`: runtime backend. Default: `auto
|
|
519
|
+
- `--runtime <auto|llama-server|llama-cpp|lmstudio|vllm|openai-compatible>`: runtime backend. Default: `auto`, which prefers llama.cpp
|
|
206
520
|
- `--provider <id>`: catalog provider id to use, for example `lmstudio` or `vllm`
|
|
207
521
|
- `--model <alias|id|path|auto>`: model alias, model id, or GGUF path
|
|
208
522
|
- `--ctx <n>` / `--context-window <n>`: model context window
|
|
209
523
|
- `--max-tokens <n>`: generated model max output tokens
|
|
524
|
+
- `--continue-on-truncation <n>`: continue a reply cut off by the output limit, up to `n` times. Off by default, and `LOCALPI_CONTINUE_ON_TRUNCATION=<n>` sets the same limit
|
|
210
525
|
- `--base-url <url>`: OpenAI-compatible endpoint for LM Studio or custom endpoints
|
|
211
526
|
- `--server-command <path>`: `llama-server` executable path
|
|
212
527
|
- `--llama-server <path>`: alias for `--server-command`
|
|
@@ -217,24 +532,50 @@ demowall record --session demowall-<timestamp> --out demo.mp4 --seconds 60
|
|
|
217
532
|
- `--chat-template <path>`: optional llama.cpp chat template file
|
|
218
533
|
- `--state-dir <path>`: runtime state directory. Default: `~/.local/state/localpi`
|
|
219
534
|
- `--session-dir <path>`: Pi session directory. Default: `<state-dir>/sessions`
|
|
220
|
-
- `--pi-command <command>`: Pi launch command
|
|
535
|
+
- `--pi-command <command>`: Pi launch command as a program and its arguments. Quotes group words, and the command runs without a shell. Default: `npx -y @earendil-works/pi-coding-agent@latest`, so a normal launch runs the newest Pi release
|
|
221
536
|
- `--providers-file <path>`: provider registry JSON
|
|
222
537
|
- `--model-profile <path>`: local model capability profile JSON
|
|
223
538
|
- `--model-reasoning <bool>`: override generated Pi reasoning capability
|
|
224
539
|
- `--model-thinking-format <deepseek|qwen-chat-template>`: override generated Pi thinking format
|
|
225
540
|
- `--tools <list>`: Pi tools allow list. Default: `read,bash,edit,write,grep,find,ls`
|
|
226
541
|
- `--thinking <off|minimal|low|medium|high|xhigh>`: Pi thinking level and managed `llama-server` reasoning budget. Default: last saved level, then `medium`
|
|
542
|
+
- `--thinking-budget <n>`: managed `llama-server` thinking cap in tokens. `-1` leaves thinking unrestricted, and a positive value replaces the budget of the thinking level
|
|
543
|
+
- `--thinking-budget-message <text>`: text the server injects before the end-of-thinking tag when the budget runs out. An empty value passes no message. Default: `Reasoning budget reached. Stop thinking and answer now.`
|
|
227
544
|
- `--demo`: endlessly run Pi prompts inside the normal Pi TUI until interrupted or Pi exits; requires an explicit non-`auto` model
|
|
228
545
|
- `--demo-initial-prompt <text>`: first demo prompt
|
|
229
546
|
- `--demo-followup-prompt <text>`: repeated demo prompt after the first run
|
|
230
547
|
- `--demo-initial-prompt-file <path>`: UTF-8 file for the first demo prompt
|
|
231
548
|
- `--demo-followup-prompt-file <path>`: UTF-8 file for repeated demo prompts
|
|
232
|
-
- `--
|
|
233
|
-
- `--no-
|
|
549
|
+
- `--acp`: serve the Agent Client Protocol on stdio through the pinned `pi-acp` adapter; requires an explicit non-`auto` model
|
|
550
|
+
- `--no-approval`: start with the tool approval gate off for the session
|
|
551
|
+
- `--approve-read-tools`: also ask before read-only tools (`read`, `grep`, `find`, `ls`)
|
|
552
|
+
- `--stats <off|line|full>`: status detail level. Default: `full`, or the last saved `/stats` choice
|
|
553
|
+
- `--skills <own|ambient|off>`: skill sources. Default: `own`, which loads only `<state-dir>/pi-skills/` and turns off shared discovery such as `~/.agents/skills`. `ambient` keeps Pi's own discovery, and `off` loads no skills
|
|
554
|
+
- `--no-skills`: load no skills. Alias for `--skills off`
|
|
555
|
+
- `--no-token-status`: disable the token status extension. Alias for `--stats off`
|
|
234
556
|
- `--status`: print runtime, model, and Pi config status
|
|
235
557
|
- `--stop`: stop the managed `llama-server` process
|
|
236
558
|
- `--list`: list configured model aliases
|
|
237
559
|
|
|
560
|
+
## Pi Helper Tools
|
|
561
|
+
|
|
562
|
+
Pi manages two helper binaries, `fd` and `rg`, and downloads them into its own bin directory when
|
|
563
|
+
they are missing from `PATH`. Localpi starts Pi in offline mode, so Pi skips that download. A start
|
|
564
|
+
that shows
|
|
565
|
+
|
|
566
|
+
```
|
|
567
|
+
Warning: fd not found. Offline mode enabled, skipping download.
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
means `fd` is not installed, and Pi falls back to a slower file search. Fix it in either way:
|
|
571
|
+
|
|
572
|
+
- Install `fd`, for example with `brew install fd`. Pi uses the `fd` that is in `PATH`.
|
|
573
|
+
- Run `PI_OFFLINE=0 localpi` once, and let Pi download `fd` into its own bin directory.
|
|
574
|
+
|
|
575
|
+
Localpi keeps offline mode on by default, because a local-model session should not make network
|
|
576
|
+
calls without a reason. `PI_OFFLINE` is pi-factory's default and passes straight through, so an
|
|
577
|
+
explicit `PI_OFFLINE=0` or `PI_OFFLINE=1` always wins.
|
|
578
|
+
|
|
238
579
|
## Environment
|
|
239
580
|
|
|
240
581
|
- `LOCALPI_RUNTIME`
|
|
@@ -250,6 +591,7 @@ demowall record --session demowall-<timestamp> --out demo.mp4 --seconds 60
|
|
|
250
591
|
- `LOCALPI_PI_CMD`
|
|
251
592
|
- `LOCALPI_CONTEXT_WINDOW`
|
|
252
593
|
- `LOCALPI_MAX_TOKENS`
|
|
594
|
+
- `LOCALPI_CONTINUE_ON_TRUNCATION`
|
|
253
595
|
- `LOCALPI_LLAMA_SERVER`
|
|
254
596
|
- `LOCALPI_HOST`
|
|
255
597
|
- `LOCALPI_PORT`
|
|
@@ -258,6 +600,13 @@ demowall record --session demowall-<timestamp> --out demo.mp4 --seconds 60
|
|
|
258
600
|
- `LOCALPI_CHAT_TEMPLATE`
|
|
259
601
|
- `LOCALPI_TOOLS`
|
|
260
602
|
- `LOCALPI_THINKING`
|
|
603
|
+
- `LOCALPI_THINKING_BUDGET`
|
|
604
|
+
- `LOCALPI_THINKING_BUDGET_MESSAGE`
|
|
605
|
+
- `LOCALPI_STATS`
|
|
606
|
+
- `LOCALPI_SKILLS`
|
|
607
|
+
- `LOCALPI_APPROVE_READ_TOOLS`
|
|
608
|
+
- `LOCALPI_ACP`
|
|
609
|
+
- `LOCALPI_ACP_ADAPTER`
|
|
261
610
|
- `LOCALPI_DEMO`
|
|
262
611
|
- `LOCALPI_DEMO_INITIAL_PROMPT`
|
|
263
612
|
- `LOCALPI_DEMO_FOLLOWUP_PROMPT`
|
|
@@ -330,3 +679,6 @@ npm test
|
|
|
330
679
|
npm run build
|
|
331
680
|
npm run check
|
|
332
681
|
```
|
|
682
|
+
|
|
683
|
+
`npm run check` is the default gate. It does not run mutation testing, because the mutation run takes
|
|
684
|
+
minutes. Run `npm run mutate` by hand once in a while; the `mutation` workflow runs it weekly.
|
package/dist/src/cli/cli.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { runPiApp } from "@osolmaz/pi-factory";
|
|
2
|
+
import { paint } from "../localpi/catppuccin.js";
|
|
2
3
|
import { errorMessage, fail, ok } from "../common/result.js";
|
|
4
|
+
import { runAcpApp } from "../localpi/acp.js";
|
|
3
5
|
import { parseLocalpiArgs, usage } from "../localpi/options.js";
|
|
6
|
+
import { engineEntries, providerConfigs } from "../localpi/provider-registry.js";
|
|
4
7
|
import { aliasListOutput, connectionStatus, resolveRuntime, statusOutput, stopRuntime } from "../localpi/runtime.js";
|
|
5
8
|
import { applyRememberedSettings } from "../localpi/settings-state.js";
|
|
6
9
|
import { createLocalpiAppDefinition } from "../pi/app.js";
|
|
10
|
+
import { writeLocalpiTheme } from "../pi/theme.js";
|
|
7
11
|
import { writeDefaultExtensions } from "../pi/extensions.js";
|
|
12
|
+
import { ensureLocalpiSkillsDir } from "../pi/skills.js";
|
|
8
13
|
export async function run(args) {
|
|
9
14
|
try {
|
|
10
15
|
let options = parseLocalpiArgs(args);
|
|
@@ -12,24 +17,36 @@ export async function run(args) {
|
|
|
12
17
|
if (helpResult !== undefined) {
|
|
13
18
|
return helpResult;
|
|
14
19
|
}
|
|
20
|
+
validateAcpOptions(options);
|
|
15
21
|
validateDemoOptions(options);
|
|
16
22
|
const commandResult = await immediateCommandResult(options);
|
|
17
23
|
if (commandResult !== undefined) {
|
|
18
24
|
return commandResult;
|
|
19
25
|
}
|
|
20
26
|
options = await applyRememberedSettings(options, {
|
|
21
|
-
thinking: hasExplicitThinkingOverride(args)
|
|
27
|
+
thinking: hasExplicitThinkingOverride(args),
|
|
28
|
+
stats: hasExplicitStatsOverride(args),
|
|
29
|
+
permission: hasExplicitPermissionOverride(args)
|
|
22
30
|
});
|
|
23
31
|
const connection = await resolveRuntime(options);
|
|
24
32
|
const selectorOptions = startupModelSelectorOptions(options, connection);
|
|
25
33
|
const extensions = await writeDefaultExtensions(options, {
|
|
26
|
-
...(selectorOptions === undefined ? {} : { startupModelSelector: selectorOptions })
|
|
34
|
+
...(selectorOptions === undefined ? {} : { startupModelSelector: selectorOptions }),
|
|
35
|
+
runtime: {
|
|
36
|
+
providerId: connection.providerId,
|
|
37
|
+
baseUrl: connection.baseUrl,
|
|
38
|
+
model: connection.model
|
|
39
|
+
},
|
|
40
|
+
engines: engineEntries(await providerConfigs(options))
|
|
27
41
|
});
|
|
28
|
-
|
|
29
|
-
|
|
42
|
+
await ensureLocalpiSkillsDir(options);
|
|
43
|
+
const app = createLocalpiAppDefinition(options, connection, extensions, await writeLocalpiTheme(options.stateDir, options.forwardedArgs));
|
|
44
|
+
return options.acp
|
|
45
|
+
? await launchAcpRuntime(app, connection)
|
|
46
|
+
: await launchResolvedRuntime(app, connection);
|
|
30
47
|
}
|
|
31
48
|
catch (error) {
|
|
32
|
-
return fail(
|
|
49
|
+
return fail(`${paint("localpi:", "red")} ${errorMessage(error)}`);
|
|
33
50
|
}
|
|
34
51
|
}
|
|
35
52
|
function validateDemoOptions(options) {
|
|
@@ -76,6 +93,52 @@ function validateDemoTty() {
|
|
|
76
93
|
throw new Error("--demo requires an interactive TTY on stdin and stdout; run it directly in a terminal");
|
|
77
94
|
}
|
|
78
95
|
}
|
|
96
|
+
function validateAcpOptions(options) {
|
|
97
|
+
if (!options.acp) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// A command-line immediate command wins over an environment-set LOCALPI_ACP, as it does for demo
|
|
101
|
+
// mode, so an exported mode switch does not take the status, stop, and list commands away.
|
|
102
|
+
if (!options.acpFromCli && hasImmediateCommand(options)) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (options.demo) {
|
|
106
|
+
throw new Error("--acp cannot be used with --demo; ACP mode owns the terminal");
|
|
107
|
+
}
|
|
108
|
+
validateAcpImmediateOptions(options);
|
|
109
|
+
validateAcpModel(options);
|
|
110
|
+
validateForwardedAcpOptions(options.forwardedArgs);
|
|
111
|
+
}
|
|
112
|
+
function validateAcpImmediateOptions(options) {
|
|
113
|
+
if (options.status) {
|
|
114
|
+
throw new Error("--acp cannot be used with --status");
|
|
115
|
+
}
|
|
116
|
+
if (options.stop) {
|
|
117
|
+
throw new Error("--acp cannot be used with --stop");
|
|
118
|
+
}
|
|
119
|
+
if (options.list) {
|
|
120
|
+
throw new Error("--acp cannot be used with --list");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function validateAcpModel(options) {
|
|
124
|
+
if (options.model === undefined || options.model === "auto") {
|
|
125
|
+
throw new Error("--acp requires an explicit --model <alias|id|path> or LOCALPI_MODEL value; ACP mode has no terminal for model selection");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function validateForwardedAcpOptions(args) {
|
|
129
|
+
const incompatibleMode = forwardedIncompatibleMode(args);
|
|
130
|
+
if (incompatibleMode !== undefined) {
|
|
131
|
+
throw new Error(`--acp cannot be used with forwarded Pi mode ${incompatibleMode}; the ACP adapter starts Pi in rpc mode`);
|
|
132
|
+
}
|
|
133
|
+
const sessionFlag = forwardedSessionFlag(args);
|
|
134
|
+
if (sessionFlag !== undefined) {
|
|
135
|
+
throw new Error(`--acp cannot be used with forwarded Pi session flag ${sessionFlag}; the ACP client manages sessions`);
|
|
136
|
+
}
|
|
137
|
+
const promptInput = forwardedPromptInput(args);
|
|
138
|
+
if (promptInput !== undefined) {
|
|
139
|
+
throw new Error(`--acp cannot be used with forwarded Pi prompt input ${promptInput}; ACP clients send prompts over the protocol`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
79
142
|
function forwardedIncompatibleMode(args) {
|
|
80
143
|
for (let index = 0; index < args.length; index += 1) {
|
|
81
144
|
const arg = args[index];
|
|
@@ -246,6 +309,16 @@ async function launchResolvedRuntime(app, connection) {
|
|
|
246
309
|
}
|
|
247
310
|
return ok(connection.warnings.length === 0 ? "" : connectionStatus(connection));
|
|
248
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* ACP mode keeps stdout for the protocol only, so localpi reports its own diagnostics on stderr and
|
|
314
|
+
* returns the adapter's exit code unchanged.
|
|
315
|
+
*/
|
|
316
|
+
async function launchAcpRuntime(app, connection) {
|
|
317
|
+
const code = await runAcpApp(app, {
|
|
318
|
+
diagnostics: connection.warnings.length === 0 ? [] : [connectionStatus(connection)]
|
|
319
|
+
});
|
|
320
|
+
return { code, stdout: "", stderr: "" };
|
|
321
|
+
}
|
|
249
322
|
async function immediateCommandResult(options) {
|
|
250
323
|
if (options.list) {
|
|
251
324
|
return ok(`${await aliasListOutput()}\n`);
|
|
@@ -260,6 +333,37 @@ function helpCommandResult(options) {
|
|
|
260
333
|
? ok(usage())
|
|
261
334
|
: undefined;
|
|
262
335
|
}
|
|
336
|
+
function hasExplicitStatsOverride(args) {
|
|
337
|
+
if (process.env["LOCALPI_STATS"] !== undefined) {
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
if (process.env["LOCALPI_TOKEN_STATUS"] !== undefined) {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
for (const arg of args) {
|
|
344
|
+
if (arg === "--") {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
if (arg === "--stats" || arg === "--no-token-status") {
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
function hasExplicitPermissionOverride(args) {
|
|
354
|
+
if (process.env["LOCALPI_APPROVAL"] !== undefined) {
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
for (const arg of args) {
|
|
358
|
+
if (arg === "--") {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
if (arg === "--no-approval") {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
263
367
|
function hasExplicitThinkingOverride(args) {
|
|
264
368
|
if (process.env["LOCALPI_THINKING"] !== undefined) {
|
|
265
369
|
return true;
|
|
@@ -275,6 +379,9 @@ function hasExplicitThinkingOverride(args) {
|
|
|
275
379
|
return false;
|
|
276
380
|
}
|
|
277
381
|
function startupModelSelectorOptions(options, connection) {
|
|
382
|
+
if (options.acp) {
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
278
385
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
279
386
|
return undefined;
|
|
280
387
|
}
|