localpi 0.5.0 → 0.5.2

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 CHANGED
@@ -18,6 +18,12 @@ See:
18
18
  npm install -g localpi
19
19
  ```
20
20
 
21
+ Or the latest GitHub release directly:
22
+
23
+ ```bash
24
+ npm install -g https://github.com/osolmaz/localpi/releases/latest/download/localpi.tgz
25
+ ```
26
+
21
27
  During development:
22
28
 
23
29
  ```bash
@@ -74,6 +80,14 @@ Localpi launches Pi with:
74
80
 
75
81
  The approval gate makes failed or denied tool calls explicit to the model so the model does not claim that a blocked command ran.
76
82
 
83
+ ## Diffusion Canvas Visualizer
84
+
85
+ The live diffusion canvas visualizer (watching DiffusionGemma denoise its
86
+ answer in the TUI) lives in its own project now:
87
+ [diffusionpi](https://github.com/osolmaz/diffusionpi). It is a pi-factory app
88
+ bundle plus a standalone Pi widget package; the widget also installs into any
89
+ Pi session via `pi install`.
90
+
77
91
  ## LM Studio Alternative
78
92
 
79
93
  LM Studio exposes an OpenAI-compatible endpoint, usually:
@@ -115,7 +129,7 @@ Run an endless TUI demo:
115
129
  localpi --demo --model gemma-e4b
116
130
  ```
117
131
 
118
- Demo mode requires an explicit model, opens the normal Pi TUI, and keeps one live Pi session so followup prompts continue from the first prompt while Pi owns streaming, tok/s status, slash commands, and exit behavior.
132
+ Demo mode requires an explicit model, opens the normal Pi TUI, and keeps one live Pi session so followup prompts continue from the first prompt while Pi owns streaming, tok/s status, slash commands, and exit behavior. Under the hood it loads the shared [pi-demo-mode](https://github.com/osolmaz/pi-demo-mode) extension, configured through the `--demo-*` flags below.
119
133
 
120
134
  Override the demo prompts:
121
135
 
@@ -174,6 +188,18 @@ Stop the managed `llama-server` runtime:
174
188
  localpi --stop
175
189
  ```
176
190
 
191
+ ## Demo Grid and Recording
192
+
193
+ The `localpi grid` and `localpi record` subcommands moved to
194
+ [demowall](https://github.com/osolmaz/demowall), a standalone tool that runs
195
+ N copies of any command in a tiled tmux wall and records tmux sessions to
196
+ video. A wall of localpi demo sessions is:
197
+
198
+ ```bash
199
+ demowall grid --concurrency 4 --start -- localpi --demo --model gemma-e4b
200
+ demowall record --session demowall-<timestamp> --out demo.mp4 --seconds 60
201
+ ```
202
+
177
203
  ## Options
178
204
 
179
205
  - `--runtime <auto|llama-server|lmstudio|vllm|openai-compatible>`: runtime backend. Default: `auto`
@@ -1,4 +1,4 @@
1
- import { runPiApp } from "@dutifuldev/pi-factory";
1
+ import { runPiApp } from "@osolmaz/pi-factory";
2
2
  import { errorMessage, fail, ok } from "../common/result.js";
3
3
  import { parseLocalpiArgs, usage } from "../localpi/options.js";
4
4
  import { aliasListOutput, connectionStatus, resolveRuntime, statusOutput, stopRuntime } from "../localpi/runtime.js";
@@ -22,7 +22,9 @@ export async function run(args) {
22
22
  });
23
23
  const connection = await resolveRuntime(options);
24
24
  const selectorOptions = startupModelSelectorOptions(options, connection);
25
- const extensions = await writeDefaultExtensions(options, selectorOptions === undefined ? {} : { startupModelSelector: selectorOptions });
25
+ const extensions = await writeDefaultExtensions(options, {
26
+ ...(selectorOptions === undefined ? {} : { startupModelSelector: selectorOptions })
27
+ });
26
28
  const app = createLocalpiAppDefinition(options, connection, extensions);
27
29
  return await launchResolvedRuntime(app, connection);
28
30
  }
File without changes
@@ -143,6 +143,7 @@ function extensionConfig(extensions) {
143
143
  }
144
144
  return {
145
145
  extensions: extensions.paths.map((extensionPath) => ({ path: extensionPath })),
146
- appendSystemPrompts: [extensions.systemPrompt]
146
+ appendSystemPrompts: [extensions.systemPrompt],
147
+ ...(Object.keys(extensions.env).length === 0 ? {} : { env: extensions.env })
147
148
  };
148
149
  }
@@ -1,6 +1,9 @@
1
- export function tokenStatusExtensionSource() {
1
+ export function tokenStatusExtensionSource(options = {}) {
2
+ const includeContext = options.includeContext ?? true;
2
3
  return `import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
4
 
5
+ const includeContext = ${JSON.stringify(includeContext)};
6
+
4
7
  type Usage = {
5
8
  input?: number;
6
9
  output?: number;
@@ -74,9 +77,10 @@ export default function localpiTokenStatus(pi: ExtensionAPI): void {
74
77
  const elapsedSeconds = elapsed(state, now);
75
78
  const decodeSeconds = generationElapsed(state, now);
76
79
  const prefillText = prefillStatusText(state, input, cacheWrite);
77
- const context = ctx.getContextUsage();
78
- const contextText =
79
- context && context.percent !== null
80
+ const context = includeContext ? ctx.getContextUsage() : undefined;
81
+ const contextText = !includeContext
82
+ ? undefined
83
+ : context && context.percent !== null
80
84
  ? \`ctx \${Math.round(context.percent)}%/\${Math.round(context.contextWindow / 1000)}k\`
81
85
  : "ctx ?";
82
86
 
@@ -1,8 +1,8 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
2
3
  import path from "node:path";
3
4
  import { localpiSettingsPath } from "../localpi/settings-state.js";
4
5
  import { resolveDemoPrompts } from "./demo.js";
5
- import { demoModeExtensionSource } from "./extension-sources/demo-mode.js";
6
6
  import { startupModelSelectorExtensionSource } from "./extension-sources/startup-model-selector.js";
7
7
  import { thinkingControlExtensionSource } from "./extension-sources/thinking-control.js";
8
8
  import { tokenStatusExtensionSource } from "./extension-sources/token-status.js";
@@ -11,24 +11,40 @@ export async function writeDefaultExtensions(options, extensionOptions = {}) {
11
11
  const extensionDir = path.join(options.stateDir, "pi-extensions");
12
12
  await mkdir(extensionDir, { recursive: true });
13
13
  const paths = [];
14
+ let env = {};
14
15
  if (extensionOptions.startupModelSelector !== undefined) {
15
16
  paths.push(await writeExtension(extensionDir, "startup-model-selector.ts", startupModelSelectorExtensionSource(extensionOptions.startupModelSelector)));
16
17
  }
17
18
  if (options.demo) {
18
- paths.push(await writeExtension(extensionDir, "demo-mode.ts", demoModeExtensionSource(await resolveDemoPrompts(options))));
19
+ paths.push(demoModeExtensionPath());
20
+ const prompts = await resolveDemoPrompts(options);
21
+ env = {
22
+ ...env,
23
+ PI_DEMO_MODE: "1",
24
+ PI_DEMO_INITIAL_PROMPT: prompts.initial,
25
+ PI_DEMO_FOLLOWUP_PROMPT: prompts.followup
26
+ };
19
27
  }
20
28
  paths.push(await writeExtension(extensionDir, "thinking-control.ts", thinkingControlExtensionSource(localpiSettingsPath(options))));
21
29
  if (options.approval) {
22
30
  paths.push(await writeExtension(extensionDir, "tool-approval.ts", approvalExtensionSource()));
23
31
  }
24
32
  if (options.tokenStatus) {
25
- paths.push(await writeExtension(extensionDir, "token-status.ts", tokenStatusExtensionSource()));
33
+ paths.push(await writeExtension(extensionDir, "token-status.ts", tokenStatusExtensionSource({ includeContext: !options.demo })));
26
34
  }
27
35
  return {
28
36
  paths,
37
+ env,
29
38
  systemPrompt: localpiSystemPrompt(options.approval)
30
39
  };
31
40
  }
41
+ // Demo mode is the shared pi-demo-mode package (a git dependency), loaded
42
+ // straight from node_modules and configured through PI_DEMO_* env vars.
43
+ export function demoModeExtensionPath() {
44
+ const require = createRequire(import.meta.url);
45
+ const packageJson = require.resolve("pi-demo-mode/package.json");
46
+ return path.join(path.dirname(packageJson), "extensions", "demo-mode.ts");
47
+ }
32
48
  async function writeExtension(extensionDir, name, source) {
33
49
  const extensionPath = path.join(extensionDir, name);
34
50
  await writeFile(extensionPath, source, "utf8");
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "localpi",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Swiss army knife for running Pi with local inference engines.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/dutifuldev/localpi.git"
9
+ "url": "git+https://github.com/osolmaz/localpi.git"
10
10
  },
11
11
  "bugs": {
12
- "url": "https://github.com/dutifuldev/localpi/issues"
12
+ "url": "https://github.com/osolmaz/localpi/issues"
13
13
  },
14
- "homepage": "https://github.com/dutifuldev/localpi#readme",
14
+ "homepage": "https://github.com/osolmaz/localpi#readme",
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
@@ -26,7 +26,8 @@
26
26
  "LICENSE"
27
27
  ],
28
28
  "scripts": {
29
- "build": "tsc -p tsconfig.json",
29
+ "build": "tsc -p tsconfig.json && chmod +x dist/src/cli/main.js",
30
+ "prepare": "npm run build",
30
31
  "format": "prettier --check .",
31
32
  "lint": "eslint .",
32
33
  "typecheck": "tsc --noEmit",
@@ -48,13 +49,14 @@
48
49
  "@vitest/coverage-v8": "^3.0.0",
49
50
  "eslint": "^9.0.0",
50
51
  "prettier": "^3.0.0",
51
- "slophammer-ts": "0.4.0",
52
+ "slophammer-ts": "0.4.1",
52
53
  "tsx": "^4.19.4",
53
54
  "typescript": "^5.0.0",
54
55
  "typescript-eslint": "^8.0.0",
55
56
  "vitest": "^3.0.0"
56
57
  },
57
58
  "dependencies": {
58
- "@dutifuldev/pi-factory": "^0.1.1"
59
+ "@osolmaz/pi-factory": "^0.1.2",
60
+ "pi-demo-mode": "github:osolmaz/pi-demo-mode#v0.1.0"
59
61
  }
60
62
  }
@@ -1,110 +0,0 @@
1
- export function demoModeExtensionSource(prompts) {
2
- const initialPromptSource = JSON.stringify(prompts.initial);
3
- const followupPromptSource = JSON.stringify(prompts.followup);
4
- return `import type { ExtensionAPI, ExtensionContext, TurnEndEvent } from "@earendil-works/pi-coding-agent";
5
-
6
- const initialPrompt = ${initialPromptSource};
7
- const followupPrompt = ${followupPromptSource};
8
- const compactAtContextPercent = 70;
9
- const demoCompactionInstructions = [
10
- "Preserve the demo narrative state, named entities, current setting,",
11
- "unresolved plot threads, and latest user direction.",
12
- "Keep the summary concise so the story can continue after compaction."
13
- ].join(" ");
14
-
15
- export default function localpiDemoMode(pi: ExtensionAPI): void {
16
- let started = false;
17
- let stopped = false;
18
- let compacting = false;
19
-
20
- function queueInitialPrompt(): void {
21
- queueMicrotask(() => {
22
- if (!stopped) {
23
- pi.sendUserMessage(initialPrompt);
24
- }
25
- });
26
- }
27
-
28
- function queueFollowup(): void {
29
- queueMicrotask(() => {
30
- if (!stopped && !compacting) {
31
- pi.sendUserMessage(followupPrompt, { deliverAs: "followUp" });
32
- }
33
- });
34
- }
35
-
36
- function compactThenFollowup(ctx: ExtensionContext): void {
37
- if (compacting) {
38
- return;
39
- }
40
- compacting = true;
41
- ctx.compact({
42
- customInstructions: demoCompactionInstructions,
43
- onComplete: () => {
44
- compacting = false;
45
- queueFollowup();
46
- },
47
- onError: (error) => {
48
- compacting = false;
49
- stopped = true;
50
- ctx.ui.notify("Demo compaction failed: " + error.message, "error");
51
- }
52
- });
53
- }
54
-
55
- pi.on("session_start", (event, ctx) => {
56
- if (started || stopped || event.reason !== "startup" || ctx.mode !== "tui") {
57
- return;
58
- }
59
- started = true;
60
- queueInitialPrompt();
61
- });
62
-
63
- pi.on("turn_end", (event, ctx) => {
64
- if (!started || stopped || compacting || ctx.mode !== "tui") {
65
- return;
66
- }
67
- if (event.message.role !== "assistant") {
68
- return;
69
- }
70
- switch (event.message.stopReason) {
71
- case "aborted":
72
- case "error":
73
- stopped = true;
74
- return;
75
- case "toolUse":
76
- return;
77
- }
78
- if (shouldCompactBeforeFollowup(event, ctx)) {
79
- compactThenFollowup(ctx);
80
- return;
81
- }
82
- queueFollowup();
83
- });
84
-
85
- pi.on("session_shutdown", () => {
86
- stopped = true;
87
- });
88
- }
89
-
90
- function shouldCompactBeforeFollowup(event: TurnEndEvent, ctx: ExtensionContext): boolean {
91
- const contextPercent = currentContextPercent(event, ctx);
92
- return contextPercent !== undefined && contextPercent >= compactAtContextPercent;
93
- }
94
-
95
- function currentContextPercent(event: TurnEndEvent, ctx: ExtensionContext): number | undefined {
96
- const usage = ctx.getContextUsage();
97
- if (usage?.percent !== undefined && usage.percent !== null) {
98
- return usage.percent;
99
- }
100
- if (event.message.role !== "assistant") {
101
- return undefined;
102
- }
103
- const contextWindow = ctx.model?.contextWindow;
104
- if (contextWindow === undefined || contextWindow <= 0) {
105
- return undefined;
106
- }
107
- return (event.message.usage.totalTokens / contextWindow) * 100;
108
- }
109
- `;
110
- }