@aliou/pi-neuralwatt 0.0.1 → 0.1.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 ADDED
@@ -0,0 +1,137 @@
1
+ ![banner](https://assets.aliou.me/pi-extensions/banners/pi-neuralwatt.png)
2
+
3
+ # Pi Neuralwatt Extension
4
+
5
+ A Pi extension that adds [Neuralwatt](https://portal.neuralwatt.com/auth/register?ref=NW-ALIOU-Q7MF) as a model provider, giving you access to open-source models through an OpenAI-compatible API with energy transparency.
6
+
7
+ ## Installation
8
+
9
+ ### Get API Key
10
+
11
+ Sign up at [neuralwatt.com](https://portal.neuralwatt.com/auth/register?ref=NW-ALIOU-Q7MF) to get an API key.
12
+
13
+ ### Configure Credentials
14
+
15
+ The extension uses Pi's credential storage. Add your API key to `~/.pi/agent/auth.json` (recommended):
16
+
17
+ ```json
18
+ {
19
+ "neuralwatt": { "type": "api_key", "key": "your-api-key-here" }
20
+ }
21
+ ```
22
+
23
+ Or set environment variable:
24
+
25
+ ```bash
26
+ export NEURALWATT_API_KEY="your-api-key-here"
27
+ ```
28
+
29
+ ### Install Extension
30
+
31
+ ```bash
32
+ # From npm
33
+ pi install npm:@aliou/pi-neuralwatt
34
+
35
+ # From git
36
+ pi install git:github.com/aliou/pi-neuralwatt
37
+
38
+ # Local development
39
+ pi -e ./src/extensions/provider/index.ts
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ Once installed, select `neuralwatt` as your provider and choose from available models:
45
+
46
+ ```
47
+ /model neuralwatt meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8
48
+ ```
49
+
50
+ ### Quota Command
51
+
52
+ Check your API usage at a glance:
53
+
54
+ ```
55
+ /neuralwatt:quota
56
+ ```
57
+
58
+ The quota command shows three tabs:
59
+ - **Subscription** (when subscribed) — plan details, energy quota with progress bar, billing period
60
+ - **Credits** — credit balance with progress bar, accounting method
61
+ - **Usage & Key** — monthly usage (cost, requests, tokens, energy), API key info, key allowance, rate limits
62
+
63
+ https://github.com/user-attachments/assets/a8994940-c467-4744-a0f2-833cb63923ff
64
+
65
+ ### Quota Warnings
66
+
67
+ When enabled, the extension notifies you when credits or energy are running low. When you have an active subscription, only energy warnings fire (credits are on-demand top-up only). Warnings use escalation on severity transitions and have a cooldown for `warning` level.
68
+
69
+ ### Sub-bar Integration
70
+
71
+ When a Neuralwatt model is active, the footer status bar shows live quota usage (credits and energy). The status updates after each response and on session start.
72
+
73
+ ## Settings
74
+
75
+ Configure features with `/neuralwatt:settings`:
76
+
77
+ - **Quota command** — Show/hide `/neuralwatt:quota`
78
+ - **Quota warnings** — Enable/disable low quota notifications
79
+ - **Sub-bar integration** — Show/hide usage in status bar
80
+
81
+ The provider itself cannot be disabled — it is always loaded.
82
+
83
+ ## Adding or Updating Models
84
+
85
+ Models are hardcoded in `src/extensions/provider/models.ts` and validated against the live API. To update:
86
+
87
+ 1. Run `pnpm test` — it fetches `/v1/models` and compares against hardcoded definitions
88
+ 2. Fix any discrepancies (missing models, changed context windows)
89
+ 3. Re-run `pnpm test` to confirm
90
+
91
+ ## Development
92
+
93
+ ### Setup
94
+
95
+ ```bash
96
+ git clone https://github.com/aliou/pi-neuralwatt.git
97
+ cd pi-neuralwatt
98
+
99
+ # Install dependencies (sets up pre-commit hooks)
100
+ pnpm install && pnpm prepare
101
+ ```
102
+
103
+ Pre-commit hooks run on every commit:
104
+ - TypeScript type checking
105
+ - Biome linting
106
+ - Biome formatting with auto-fix
107
+
108
+ ### Commands
109
+
110
+ ```bash
111
+ # Type check
112
+ pnpm run typecheck
113
+
114
+ # Lint
115
+ pnpm run lint
116
+
117
+ # Format
118
+ pnpm run format
119
+
120
+ # Test
121
+ pnpm run test
122
+ ```
123
+
124
+ ## Release
125
+
126
+ This repository uses [Changesets](https://github.com/changesets/changesets) for versioning.
127
+
128
+ ## Requirements
129
+
130
+ - Pi coding agent v0.67.68+
131
+ - Neuralwatt API key (configured in `~/.pi/agent/auth.json` or via `NEURALWATT_API_KEY`)
132
+
133
+ ## Links
134
+
135
+ - [Neuralwatt](https://portal.neuralwatt.com/auth/register?ref=NW-ALIOU-Q7MF)
136
+ - [Neuralwatt API Docs](https://neuralwatt.com/docs)
137
+ - [Pi Documentation](https://buildwithpi.ai/)
package/package.json CHANGED
@@ -1,15 +1,78 @@
1
1
  {
2
2
  "name": "@aliou/pi-neuralwatt",
3
- "version": "0.0.1",
4
- "description": "Pi extension providing a Neuralwatt inference API provider",
3
+ "version": "0.1.1",
4
+ "license": "MIT",
5
+ "type": "module",
5
6
  "private": false,
7
+ "keywords": [
8
+ "pi-package",
9
+ "pi-extension",
10
+ "pi"
11
+ ],
6
12
  "repository": {
7
13
  "type": "git",
8
14
  "url": "https://github.com/aliou/pi-neuralwatt"
9
15
  },
10
- "license": "MIT",
11
- "main": "index.js",
16
+ "pi": {
17
+ "extensions": [
18
+ "./src/extensions/provider/index.ts",
19
+ "./src/extensions/command-quotas/index.ts",
20
+ "./src/extensions/quota-warnings/index.ts",
21
+ "./src/extensions/sub-bar-integration/index.ts"
22
+ ],
23
+ "video": "https://assets.aliou.me/pi-extensions/demos/pi-neuralwatt.mp4"
24
+ },
12
25
  "publishConfig": {
13
26
  "access": "public"
27
+ },
28
+ "files": [
29
+ "src",
30
+ "schema.json",
31
+ "README.md"
32
+ ],
33
+ "dependencies": {
34
+ "@aliou/pi-utils-settings": "^0.9.0",
35
+ "@aliou/pi-utils-ui": "^0.1.0"
36
+ },
37
+ "peerDependencies": {
38
+ "@mariozechner/pi-coding-agent": ">=0.67.68",
39
+ "@mariozechner/pi-tui": ">=0.67.68",
40
+ "@sinclair/typebox": ">=0.34.0"
41
+ },
42
+ "devDependencies": {
43
+ "@aliou/biome-plugins": "^0.8.1",
44
+ "@biomejs/biome": "^2.4.12",
45
+ "@changesets/cli": "^2.27.11",
46
+ "@mariozechner/pi-coding-agent": "0.67.68",
47
+ "@mariozechner/pi-tui": "0.67.68",
48
+ "@types/node": "^25.0.10",
49
+ "husky": "^9.1.7",
50
+ "ts-json-schema-generator": "^2.4.0",
51
+ "typescript": "^5.9.3",
52
+ "vitest": "^4.0.18"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "@mariozechner/pi-coding-agent": {
56
+ "optional": true
57
+ },
58
+ "@mariozechner/pi-tui": {
59
+ "optional": true
60
+ },
61
+ "@sinclair/typebox": {
62
+ "optional": true
63
+ }
64
+ },
65
+ "scripts": {
66
+ "typecheck": "tsc --noEmit",
67
+ "lint": "biome check",
68
+ "format": "biome check --write",
69
+ "test": "vitest run",
70
+ "test:watch": "vitest",
71
+ "gen:schema": "ts-json-schema-generator --path src/config.ts --type NeuralwattConfig --no-type-check -o schema.json",
72
+ "check:schema": "ts-json-schema-generator --path src/config.ts --type NeuralwattConfig --no-type-check -o /tmp/schema-check.json && diff -q schema.json /tmp/schema-check.json",
73
+ "check:lockfile": "pnpm install --frozen-lockfile --ignore-scripts",
74
+ "changeset": "changeset",
75
+ "version": "changeset version",
76
+ "release": "pnpm changeset publish"
14
77
  }
15
- }
78
+ }
package/schema.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "$ref": "#/definitions/NeuralwattConfig",
3
+ "$schema": "http://json-schema.org/draft-07/schema#",
4
+ "definitions": {
5
+ "NeuralwattConfig": {
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "quotaCommand": {
9
+ "description": "Show the quota command (/neuralwatt:quota).",
10
+ "type": "boolean"
11
+ },
12
+ "quotaWarnings": {
13
+ "description": "Show quota warnings when credits or energy are low.",
14
+ "type": "boolean"
15
+ },
16
+ "subBarIntegration": {
17
+ "description": "Show usage in the sub-bar / status bar.",
18
+ "type": "boolean"
19
+ }
20
+ },
21
+ "type": "object"
22
+ }
23
+ }
24
+ }
package/src/config.ts ADDED
@@ -0,0 +1,154 @@
1
+ import {
2
+ ConfigLoader,
3
+ registerSettingsCommand,
4
+ type SettingsSection,
5
+ } from "@aliou/pi-utils-settings";
6
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
+ import type { SettingItem } from "@mariozechner/pi-tui";
8
+
9
+ export type NeuralwattFeatureId =
10
+ | "quotaCommand"
11
+ | "quotaWarnings"
12
+ | "subBarIntegration";
13
+
14
+ export const NEURALWATT_EXTENSIONS_REQUEST_EVENT =
15
+ "neuralwatt:extensions:request" as const;
16
+
17
+ export const NEURALWATT_EXTENSIONS_REGISTER_EVENT =
18
+ "neuralwatt:extensions:register" as const;
19
+
20
+ export interface NeuralwattExtensionsRegisterPayload {
21
+ feature: NeuralwattFeatureId;
22
+ }
23
+
24
+ export interface NeuralwattConfig {
25
+ /** Show the quota command (/neuralwatt:quota). */
26
+ quotaCommand?: boolean;
27
+ /** Show quota warnings when credits or energy are low. */
28
+ quotaWarnings?: boolean;
29
+ /** Show usage in the sub-bar / status bar. */
30
+ subBarIntegration?: boolean;
31
+ }
32
+
33
+ export interface ResolvedNeuralwattConfig {
34
+ quotaCommand: boolean;
35
+ quotaWarnings: boolean;
36
+ subBarIntegration: boolean;
37
+ }
38
+
39
+ const DEFAULTS: ResolvedNeuralwattConfig = {
40
+ quotaCommand: true,
41
+ quotaWarnings: true,
42
+ subBarIntegration: true,
43
+ };
44
+
45
+ export const configLoader = new ConfigLoader<
46
+ NeuralwattConfig,
47
+ ResolvedNeuralwattConfig
48
+ >("neuralwatt", DEFAULTS);
49
+
50
+ export const NEURALWATT_CONFIG_UPDATED_EVENT =
51
+ "neuralwatt:config:updated" as const;
52
+
53
+ export interface NeuralwattConfigUpdatedPayload {
54
+ config: ResolvedNeuralwattConfig;
55
+ }
56
+
57
+ export function emitConfigUpdated(pi: ExtensionAPI): void {
58
+ pi.events.emit(NEURALWATT_CONFIG_UPDATED_EVENT, {
59
+ config: configLoader.getConfig(),
60
+ });
61
+ }
62
+
63
+ export interface RegisterNeuralwattSettingsOptions {
64
+ getLoadedFeatures: () => Set<NeuralwattFeatureId>;
65
+ }
66
+
67
+ function featureRow(
68
+ id: NeuralwattFeatureId,
69
+ label: string,
70
+ description: string,
71
+ configValue: boolean,
72
+ isLoaded: boolean,
73
+ ): SettingItem {
74
+ if (isLoaded) {
75
+ return {
76
+ id,
77
+ label,
78
+ description,
79
+ currentValue: configValue ? "enabled" : "disabled",
80
+ values: ["enabled", "disabled"],
81
+ };
82
+ }
83
+ return {
84
+ id,
85
+ label,
86
+ description: `${description} (Not loaded by Pi)`,
87
+ currentValue: "unavailable",
88
+ values: [],
89
+ };
90
+ }
91
+
92
+ export function registerNeuralwattSettings(
93
+ pi: ExtensionAPI,
94
+ options: RegisterNeuralwattSettingsOptions,
95
+ ): void {
96
+ const { getLoadedFeatures } = options;
97
+
98
+ registerSettingsCommand<NeuralwattConfig, ResolvedNeuralwattConfig>(pi, {
99
+ commandName: "neuralwatt:settings",
100
+ title: "Neuralwatt Settings",
101
+ configStore: configLoader,
102
+ buildSections: (tabConfig, resolved): SettingsSection[] => {
103
+ const loaded = getLoadedFeatures();
104
+ return [
105
+ {
106
+ label: "Features",
107
+ items: [
108
+ featureRow(
109
+ "quotaCommand",
110
+ "Quota command",
111
+ "Toggle the /neuralwatt:quota command, showing your API usage at a glance",
112
+ tabConfig?.quotaCommand ?? resolved.quotaCommand,
113
+ loaded.has("quotaCommand"),
114
+ ),
115
+ featureRow(
116
+ "quotaWarnings",
117
+ "Quota warnings",
118
+ "Toggle notifications when credits or energy are running low",
119
+ tabConfig?.quotaWarnings ?? resolved.quotaWarnings,
120
+ loaded.has("quotaWarnings"),
121
+ ),
122
+ featureRow(
123
+ "subBarIntegration",
124
+ "Sub-bar integration",
125
+ "Toggle integration with the status bar and sub-core",
126
+ tabConfig?.subBarIntegration ?? resolved.subBarIntegration,
127
+ loaded.has("subBarIntegration"),
128
+ ),
129
+ ],
130
+ },
131
+ ];
132
+ },
133
+ onSettingChange: (id, newValue, config) => {
134
+ if (!getLoadedFeatures().has(id as NeuralwattFeatureId)) {
135
+ return null;
136
+ }
137
+
138
+ const enabled = newValue === "enabled";
139
+ switch (id) {
140
+ case "quotaCommand":
141
+ return { ...config, quotaCommand: enabled };
142
+ case "quotaWarnings":
143
+ return { ...config, quotaWarnings: enabled };
144
+ case "subBarIntegration":
145
+ return { ...config, subBarIntegration: enabled };
146
+ default:
147
+ return null;
148
+ }
149
+ },
150
+ onSave: async () => {
151
+ emitConfigUpdated(pi);
152
+ },
153
+ });
154
+ }
@@ -0,0 +1,83 @@
1
+ import { join } from "node:path";
2
+ import { type ExtensionAPI, getAgentDir } from "@mariozechner/pi-coding-agent";
3
+ import { getNeuralwattApiKey } from "../../lib/env";
4
+ import { fetchQuotas } from "../../utils/quotas";
5
+ import { QuotasComponent } from "./components/quotas-display";
6
+
7
+ function missingAuthMessage(): string {
8
+ const authPath = join(getAgentDir(), "auth.json");
9
+ return `Neuralwatt quota requires an API key. Add credentials to ${authPath} or set the NEURALWATT_API_KEY environment variable.`;
10
+ }
11
+
12
+ export function registerQuotasCommand(pi: ExtensionAPI): void {
13
+ pi.registerCommand("neuralwatt:quota", {
14
+ description: "Display Neuralwatt API usage and quota",
15
+ handler: async (_args, ctx) => {
16
+ const apiKey = await getNeuralwattApiKey(ctx.modelRegistry.authStorage);
17
+ if (!apiKey) {
18
+ ctx.ui.notify(missingAuthMessage(), "warning");
19
+ return;
20
+ }
21
+ const key: string = apiKey;
22
+
23
+ const result = await ctx.ui.custom<null>((tui, theme, _kb, done) => {
24
+ const controller = new AbortController();
25
+ const component = new QuotasComponent(
26
+ theme,
27
+ tui,
28
+ () => {
29
+ controller.abort();
30
+ done(null);
31
+ },
32
+ () => {
33
+ component.setState({ type: "loading" });
34
+ tui.requestRender();
35
+ void loadQuotas();
36
+ },
37
+ );
38
+
39
+ async function loadQuotas(): Promise<void> {
40
+ const fetchResult = await fetchQuotas(key, controller.signal);
41
+ if (controller.signal.aborted) return;
42
+ if (fetchResult.success) {
43
+ component.setState({
44
+ type: "loaded",
45
+ quotas: fetchResult.data.quotas,
46
+ });
47
+ } else {
48
+ component.setState({
49
+ type: "error",
50
+ message: fetchResult.error.message,
51
+ });
52
+ }
53
+ tui.requestRender();
54
+ }
55
+
56
+ void loadQuotas();
57
+
58
+ return {
59
+ render: (width: number) => component.render(width),
60
+ invalidate: () => component.invalidate(),
61
+ handleInput: (data: string) => component.handleInput(data),
62
+ dispose: () => {
63
+ controller.abort();
64
+ component.destroy();
65
+ },
66
+ };
67
+ });
68
+
69
+ // Non-interactive fallback (RPC, print, JSON modes)
70
+ if (result === undefined) {
71
+ const fetchResult = await fetchQuotas(key);
72
+ if (!fetchResult.success) {
73
+ ctx.ui.notify(
74
+ JSON.stringify({ error: fetchResult.error.message }),
75
+ "error",
76
+ );
77
+ return;
78
+ }
79
+ ctx.ui.notify(JSON.stringify(fetchResult.data.quotas), "info");
80
+ }
81
+ },
82
+ });
83
+ }