@pentect/pi 0.0.28 → 0.0.30

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
@@ -1,19 +1,28 @@
1
1
  # @pentect/pi
2
2
 
3
- Run [Pi](https://github.com/badlogic/pi-mono) through Pentect's local HTTP
4
- protection without changing Pi's saved configuration.
3
+ Use [Pi](https://github.com/badlogic/pi-mono) with Pentect as a normal Pi
4
+ extension. The extension starts Pentect's local gateway and registers one
5
+ protected provider for the session.
5
6
 
6
7
  ```sh
7
- npx @pentect/pi --model openai/gpt-5
8
+ pi install npm:@pentect/pi
9
+ pi --model pentect/gpt-5
8
10
  ```
9
11
 
10
- For a permanent command:
12
+ To try it without installing:
11
13
 
12
14
  ```sh
13
- npm install --global @pentect/pi
14
- pentect-pi --model openai/gpt-5
15
+ pi -e npm:@pentect/pi --model pentect/gpt-5
15
16
  ```
16
17
 
17
- The package installs matching Pentect and Pi versions. It is a small launcher,
18
- not a prompt hook: requests go through the same loopback gateway as
19
- `pentect pi`.
18
+ Set `PENTECT_PI_MODEL` before Pi starts to expose a different model. Set
19
+ `PENTECT_PI_API=responses` for the OpenAI Responses API. `OPENAI_BASE_URL` and
20
+ `OPENAI_API_KEY` configure the upstream.
21
+
22
+ For a custom upstream whose model limits differ from the defaults, set
23
+ `PENTECT_PI_CONTEXT_WINDOW`, `PENTECT_PI_MAX_TOKENS`,
24
+ `PENTECT_PI_INPUTS=text`, or `PENTECT_PI_REASONING=true|false`. Invalid values
25
+ stop startup instead of advertising incorrect capabilities to Pi.
26
+
27
+ The JavaScript extension only manages Pi's provider lifecycle. Detection,
28
+ handles, plugins, and network forwarding remain inside the Pentect binary.
@@ -0,0 +1,210 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, resolve } from "node:path";
4
+
5
+ const require = createRequire(import.meta.url);
6
+ const STATE = Symbol.for("@pentect/pi/provider-state");
7
+ const MAX_READY_BYTES = 16 * 1024;
8
+ const START_TIMEOUT_MS = 10_000;
9
+ const DEFAULT_CONTEXT_WINDOW = 128_000;
10
+ const DEFAULT_MAX_TOKENS = 32_768;
11
+
12
+ function sharedState() {
13
+ if (!globalThis[STATE]) {
14
+ globalThis[STATE] = {
15
+ openaiApiKey: process.env.OPENAI_API_KEY,
16
+ upstreamAuthorization: process.env.PENTECT_UPSTREAM_AUTHORIZATION,
17
+ };
18
+ }
19
+ return globalThis[STATE];
20
+ }
21
+
22
+ export function providerArguments(env = process.env) {
23
+ const model = env.PENTECT_PI_MODEL?.trim() || "gpt-5";
24
+ const api = env.PENTECT_PI_API?.trim() || "chat";
25
+ return ["provider", "pi", "--model", model, "--api", api];
26
+ }
27
+
28
+ export function parseReady(line) {
29
+ let ready;
30
+ try {
31
+ ready = JSON.parse(line);
32
+ } catch {
33
+ throw new Error("Pentect provider returned invalid readiness data");
34
+ }
35
+ if (
36
+ ready?.protocol !== 1 ||
37
+ ready?.integration !== "pi" ||
38
+ typeof ready?.baseUrl !== "string" ||
39
+ !/^http:\/\/127\.0\.0\.1:\d+\/[a-f0-9]{64}\/?$/.test(ready.baseUrl) ||
40
+ typeof ready?.model !== "string" ||
41
+ !["openai-completions", "openai-responses"].includes(ready?.api)
42
+ ) {
43
+ throw new Error("Pentect provider returned unsupported readiness data");
44
+ }
45
+ return ready;
46
+ }
47
+
48
+ function positiveInteger(env, name, fallback) {
49
+ const raw = env[name]?.trim();
50
+ if (!raw) return fallback;
51
+ const value = Number(raw);
52
+ if (!Number.isSafeInteger(value) || value <= 0) {
53
+ throw new Error(`${name} must be a positive integer`);
54
+ }
55
+ return value;
56
+ }
57
+
58
+ function reasoningSupport(env, api) {
59
+ const value = env.PENTECT_PI_REASONING?.trim().toLowerCase();
60
+ if (!value || value === "auto") return api === "openai-responses";
61
+ if (value === "true") return true;
62
+ if (value === "false") return false;
63
+ throw new Error("PENTECT_PI_REASONING must be auto, true, or false");
64
+ }
65
+
66
+ function inputSupport(env) {
67
+ const value = env.PENTECT_PI_INPUTS?.trim().toLowerCase();
68
+ if (!value || value === "text,image" || value === "image,text") {
69
+ return ["text", "image"];
70
+ }
71
+ if (value === "text") return ["text"];
72
+ throw new Error("PENTECT_PI_INPUTS must be text or text,image");
73
+ }
74
+
75
+ export function providerDefinition(ready, env = process.env) {
76
+ return {
77
+ name: "Pentect",
78
+ baseUrl: ready.baseUrl,
79
+ apiKey: "pentect-local",
80
+ authHeader: true,
81
+ api: ready.api,
82
+ models: [
83
+ {
84
+ id: ready.model,
85
+ name: ready.model,
86
+ reasoning: reasoningSupport(env, ready.api),
87
+ input: inputSupport(env),
88
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
89
+ contextWindow: positiveInteger(
90
+ env,
91
+ "PENTECT_PI_CONTEXT_WINDOW",
92
+ DEFAULT_CONTEXT_WINDOW,
93
+ ),
94
+ maxTokens: positiveInteger(
95
+ env,
96
+ "PENTECT_PI_MAX_TOKENS",
97
+ DEFAULT_MAX_TOKENS,
98
+ ),
99
+ },
100
+ ],
101
+ };
102
+ }
103
+
104
+ function pentectScript() {
105
+ const manifest = require.resolve("pentect/package.json");
106
+ return resolve(dirname(manifest), "packaging", "npm", "bin", "pentect.js");
107
+ }
108
+
109
+ async function startProvider() {
110
+ const state = sharedState();
111
+ const env = { ...process.env };
112
+ if (state.openaiApiKey) env.OPENAI_API_KEY = state.openaiApiKey;
113
+ else delete env.OPENAI_API_KEY;
114
+ if (state.upstreamAuthorization) {
115
+ env.PENTECT_UPSTREAM_AUTHORIZATION = state.upstreamAuthorization;
116
+ } else {
117
+ delete env.PENTECT_UPSTREAM_AUTHORIZATION;
118
+ }
119
+
120
+ const child = spawn(
121
+ process.execPath,
122
+ [pentectScript(), ...providerArguments(env)],
123
+ { env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
124
+ );
125
+ process.env.OPENAI_API_KEY = "pentect-local";
126
+ delete process.env.PENTECT_UPSTREAM_AUTHORIZATION;
127
+
128
+ let errors = "";
129
+ child.stderr.setEncoding("utf8");
130
+ child.stderr.on("data", (chunk) => {
131
+ if (errors.length < MAX_READY_BYTES) {
132
+ errors += chunk.slice(0, MAX_READY_BYTES - errors.length);
133
+ }
134
+ });
135
+
136
+ try {
137
+ const line = await firstLine(child);
138
+ return { child, ready: parseReady(line) };
139
+ } catch (error) {
140
+ child.kill();
141
+ restoreProviderCredentials(state, process.env);
142
+ const detail = errors.trim();
143
+ throw new Error(detail || error.message, { cause: error });
144
+ }
145
+ }
146
+
147
+ export function restoreProviderCredentials(state, env) {
148
+ if (state.openaiApiKey === undefined) delete env.OPENAI_API_KEY;
149
+ else env.OPENAI_API_KEY = state.openaiApiKey;
150
+ if (state.upstreamAuthorization === undefined) {
151
+ delete env.PENTECT_UPSTREAM_AUTHORIZATION;
152
+ } else {
153
+ env.PENTECT_UPSTREAM_AUTHORIZATION = state.upstreamAuthorization;
154
+ }
155
+ }
156
+
157
+ function firstLine(child) {
158
+ return new Promise((resolveLine, reject) => {
159
+ let output = "";
160
+ const timeout = setTimeout(() => {
161
+ reject(new Error("Pentect provider did not become ready"));
162
+ }, START_TIMEOUT_MS);
163
+ timeout.unref?.();
164
+
165
+ const finish = (callback, value) => {
166
+ clearTimeout(timeout);
167
+ child.stdout.removeAllListeners();
168
+ child.removeListener("error", onError);
169
+ child.removeListener("exit", onExit);
170
+ callback(value);
171
+ };
172
+ const onError = (error) => finish(reject, error);
173
+ const onExit = (code) =>
174
+ finish(reject, new Error(`Pentect provider exited before startup (${code})`));
175
+
176
+ child.once("error", onError);
177
+ child.once("exit", onExit);
178
+ child.stdout.setEncoding("utf8");
179
+ child.stdout.on("data", (chunk) => {
180
+ output += chunk;
181
+ if (output.length > MAX_READY_BYTES) {
182
+ finish(reject, new Error("Pentect provider readiness data is too large"));
183
+ return;
184
+ }
185
+ const newline = output.indexOf("\n");
186
+ if (newline >= 0) finish(resolveLine, output.slice(0, newline));
187
+ });
188
+ });
189
+ }
190
+
191
+ function stopProvider(child) {
192
+ if (!child || child.exitCode !== null || child.killed) return;
193
+ child.stdin.end();
194
+ const timeout = setTimeout(() => child.kill(), 2_000);
195
+ timeout.unref?.();
196
+ child.once("exit", () => clearTimeout(timeout));
197
+ }
198
+
199
+ export default async function pentect(pi) {
200
+ const state = sharedState();
201
+ const { child, ready } = await startProvider();
202
+ try {
203
+ pi.registerProvider("pentect", providerDefinition(ready));
204
+ } catch (error) {
205
+ stopProvider(child);
206
+ restoreProviderCredentials(state, process.env);
207
+ throw error;
208
+ }
209
+ pi.on("session_shutdown", () => stopProvider(child));
210
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pentect/pi",
3
- "version": "0.0.28",
4
- "description": "Run Pi through Pentect's local HTTP protection",
3
+ "version": "0.0.30",
4
+ "description": "Pentect provider extension for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -17,23 +17,24 @@
17
17
  "keywords": [
18
18
  "ai-security",
19
19
  "pi-coding-agent",
20
+ "pi-package",
20
21
  "secret-detection"
21
22
  ],
22
- "bin": {
23
- "pentect-pi": "bin/pentect-pi.js"
23
+ "pi": {
24
+ "extensions": [
25
+ "extensions/pentect.js"
26
+ ]
24
27
  },
25
28
  "files": [
26
- "bin",
27
- "lib",
29
+ "extensions",
28
30
  "README.md"
29
31
  ],
30
32
  "scripts": {
31
- "check": "node --check bin/pentect-pi.js && node --check lib/command.js",
33
+ "check": "node --check extensions/pentect.js",
32
34
  "test": "node --test"
33
35
  },
34
36
  "dependencies": {
35
- "@mariozechner/pi-coding-agent": "0.73.1",
36
- "pentect": "0.0.28"
37
+ "pentect": "0.0.30"
37
38
  },
38
39
  "engines": {
39
40
  "node": ">=20.6.0"
package/bin/pentect-pi.js DELETED
@@ -1,42 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { spawnSync } from "node:child_process";
4
- import { readFileSync } from "node:fs";
5
- import { dirname, resolve } from "node:path";
6
- import { createRequire } from "node:module";
7
- import {
8
- invocation,
9
- packageEntryPath,
10
- piBinaryFromEntry,
11
- } from "../lib/command.js";
12
-
13
- const require = createRequire(import.meta.url);
14
-
15
- function packageBinary(name, binary) {
16
- const manifestPath = require.resolve(`${name}/package.json`);
17
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
18
- const relative =
19
- typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.[binary];
20
- if (typeof relative !== "string" || relative.length === 0) {
21
- throw new Error(`${name} does not provide the expected ${binary} command`);
22
- }
23
- return resolve(dirname(manifestPath), relative);
24
- }
25
-
26
- try {
27
- const pentectCli = packageBinary("pentect", "pentect");
28
- // Pi exports dist/index.js but intentionally does not export package.json.
29
- // Its pinned package exposes the adjacent dist/cli.js as the `pi` binary.
30
- const piCli = piBinaryFromEntry(
31
- packageEntryPath("@mariozechner/pi-coding-agent"),
32
- );
33
- const next = invocation(pentectCli, piCli, process.argv.slice(2));
34
- const result = spawnSync(next.command, next.args, { stdio: "inherit" });
35
- if (result.error) throw result.error;
36
- if (result.signal) process.kill(process.pid, result.signal);
37
- process.exitCode = result.status ?? 1;
38
- } catch (error) {
39
- const message = error instanceof Error ? error.message : String(error);
40
- console.error(`pentect-pi: ${message}`);
41
- process.exitCode = 1;
42
- }
package/lib/command.js DELETED
@@ -1,25 +0,0 @@
1
- import { dirname, resolve } from "node:path";
2
- import { fileURLToPath } from "node:url";
3
-
4
- export function invocation(pentectCli, piCli, userArgs, node = process.execPath) {
5
- return {
6
- command: node,
7
- args: [
8
- pentectCli,
9
- "pi",
10
- "--pi",
11
- node,
12
- "--",
13
- piCli,
14
- ...userArgs,
15
- ],
16
- };
17
- }
18
-
19
- export function piBinaryFromEntry(entry) {
20
- return resolve(dirname(entry), "cli.js");
21
- }
22
-
23
- export function packageEntryPath(specifier, resolver = import.meta.resolve) {
24
- return fileURLToPath(resolver(specifier));
25
- }