@pentect/pi 0.0.27 → 0.0.29
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 +13 -9
- package/extensions/pentect.js +155 -0
- package/package.json +10 -9
- package/bin/pentect-pi.js +0 -42
- package/lib/command.js +0 -25
package/README.md
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
# @pentect/pi
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
8
|
+
pi install npm:@pentect/pi
|
|
9
|
+
pi --model pentect/gpt-5
|
|
8
10
|
```
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
To try it without installing:
|
|
11
13
|
|
|
12
14
|
```sh
|
|
13
|
-
npm
|
|
14
|
-
pentect-pi --model openai/gpt-5
|
|
15
|
+
pi -e npm:@pentect/pi --model pentect/gpt-5
|
|
15
16
|
```
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
`
|
|
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
|
+
The JavaScript extension only manages Pi's provider lifecycle. Detection,
|
|
23
|
+
handles, plugins, and network forwarding remain inside the Pentect binary.
|
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
|
|
10
|
+
function sharedState() {
|
|
11
|
+
if (!globalThis[STATE]) {
|
|
12
|
+
globalThis[STATE] = {
|
|
13
|
+
openaiApiKey: process.env.OPENAI_API_KEY,
|
|
14
|
+
upstreamAuthorization: process.env.PENTECT_UPSTREAM_AUTHORIZATION,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return globalThis[STATE];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function providerArguments(env = process.env) {
|
|
21
|
+
const model = env.PENTECT_PI_MODEL?.trim() || "gpt-5";
|
|
22
|
+
const api = env.PENTECT_PI_API?.trim() || "chat";
|
|
23
|
+
return ["provider", "pi", "--model", model, "--api", api];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseReady(line) {
|
|
27
|
+
let ready;
|
|
28
|
+
try {
|
|
29
|
+
ready = JSON.parse(line);
|
|
30
|
+
} catch {
|
|
31
|
+
throw new Error("Pentect provider returned invalid readiness data");
|
|
32
|
+
}
|
|
33
|
+
if (
|
|
34
|
+
ready?.protocol !== 1 ||
|
|
35
|
+
ready?.integration !== "pi" ||
|
|
36
|
+
typeof ready?.baseUrl !== "string" ||
|
|
37
|
+
!/^http:\/\/127\.0\.0\.1:\d+\/[a-f0-9]{64}\/?$/.test(ready.baseUrl) ||
|
|
38
|
+
typeof ready?.model !== "string" ||
|
|
39
|
+
!["openai-completions", "openai-responses"].includes(ready?.api)
|
|
40
|
+
) {
|
|
41
|
+
throw new Error("Pentect provider returned unsupported readiness data");
|
|
42
|
+
}
|
|
43
|
+
return ready;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function providerDefinition(ready) {
|
|
47
|
+
return {
|
|
48
|
+
name: "Pentect",
|
|
49
|
+
baseUrl: ready.baseUrl,
|
|
50
|
+
apiKey: "pentect-local",
|
|
51
|
+
authHeader: true,
|
|
52
|
+
api: ready.api,
|
|
53
|
+
models: [
|
|
54
|
+
{
|
|
55
|
+
id: ready.model,
|
|
56
|
+
name: ready.model,
|
|
57
|
+
reasoning: ready.api === "openai-responses",
|
|
58
|
+
input: ["text", "image"],
|
|
59
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
60
|
+
contextWindow: 128000,
|
|
61
|
+
maxTokens: 32768,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function pentectScript() {
|
|
68
|
+
const manifest = require.resolve("pentect/package.json");
|
|
69
|
+
return resolve(dirname(manifest), "packaging", "npm", "bin", "pentect.js");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function startProvider() {
|
|
73
|
+
const state = sharedState();
|
|
74
|
+
const env = { ...process.env };
|
|
75
|
+
if (state.openaiApiKey) env.OPENAI_API_KEY = state.openaiApiKey;
|
|
76
|
+
else delete env.OPENAI_API_KEY;
|
|
77
|
+
if (state.upstreamAuthorization) {
|
|
78
|
+
env.PENTECT_UPSTREAM_AUTHORIZATION = state.upstreamAuthorization;
|
|
79
|
+
} else {
|
|
80
|
+
delete env.PENTECT_UPSTREAM_AUTHORIZATION;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const child = spawn(
|
|
84
|
+
process.execPath,
|
|
85
|
+
[pentectScript(), ...providerArguments(env)],
|
|
86
|
+
{ env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
|
|
87
|
+
);
|
|
88
|
+
process.env.OPENAI_API_KEY = "pentect-local";
|
|
89
|
+
delete process.env.PENTECT_UPSTREAM_AUTHORIZATION;
|
|
90
|
+
|
|
91
|
+
let errors = "";
|
|
92
|
+
child.stderr.setEncoding("utf8");
|
|
93
|
+
child.stderr.on("data", (chunk) => {
|
|
94
|
+
if (errors.length < MAX_READY_BYTES) {
|
|
95
|
+
errors += chunk.slice(0, MAX_READY_BYTES - errors.length);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const line = await firstLine(child);
|
|
101
|
+
return { child, ready: parseReady(line) };
|
|
102
|
+
} catch (error) {
|
|
103
|
+
child.kill();
|
|
104
|
+
const detail = errors.trim();
|
|
105
|
+
throw new Error(detail || error.message, { cause: error });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function firstLine(child) {
|
|
110
|
+
return new Promise((resolveLine, reject) => {
|
|
111
|
+
let output = "";
|
|
112
|
+
const timeout = setTimeout(() => {
|
|
113
|
+
reject(new Error("Pentect provider did not become ready"));
|
|
114
|
+
}, START_TIMEOUT_MS);
|
|
115
|
+
timeout.unref?.();
|
|
116
|
+
|
|
117
|
+
const finish = (callback, value) => {
|
|
118
|
+
clearTimeout(timeout);
|
|
119
|
+
child.stdout.removeAllListeners();
|
|
120
|
+
child.removeListener("error", onError);
|
|
121
|
+
child.removeListener("exit", onExit);
|
|
122
|
+
callback(value);
|
|
123
|
+
};
|
|
124
|
+
const onError = (error) => finish(reject, error);
|
|
125
|
+
const onExit = (code) =>
|
|
126
|
+
finish(reject, new Error(`Pentect provider exited before startup (${code})`));
|
|
127
|
+
|
|
128
|
+
child.once("error", onError);
|
|
129
|
+
child.once("exit", onExit);
|
|
130
|
+
child.stdout.setEncoding("utf8");
|
|
131
|
+
child.stdout.on("data", (chunk) => {
|
|
132
|
+
output += chunk;
|
|
133
|
+
if (output.length > MAX_READY_BYTES) {
|
|
134
|
+
finish(reject, new Error("Pentect provider readiness data is too large"));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const newline = output.indexOf("\n");
|
|
138
|
+
if (newline >= 0) finish(resolveLine, output.slice(0, newline));
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function stopProvider(child) {
|
|
144
|
+
if (!child || child.exitCode !== null || child.killed) return;
|
|
145
|
+
child.stdin.end();
|
|
146
|
+
const timeout = setTimeout(() => child.kill(), 2_000);
|
|
147
|
+
timeout.unref?.();
|
|
148
|
+
child.once("exit", () => clearTimeout(timeout));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export default async function pentect(pi) {
|
|
152
|
+
const { child, ready } = await startProvider();
|
|
153
|
+
pi.registerProvider("pentect", providerDefinition(ready));
|
|
154
|
+
pi.on("session_shutdown", () => stopProvider(child));
|
|
155
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pentect/pi",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.29",
|
|
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
|
-
"
|
|
23
|
-
"
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"extensions/pentect.js"
|
|
26
|
+
]
|
|
24
27
|
},
|
|
25
28
|
"files": [
|
|
26
|
-
"
|
|
27
|
-
"lib",
|
|
29
|
+
"extensions",
|
|
28
30
|
"README.md"
|
|
29
31
|
],
|
|
30
32
|
"scripts": {
|
|
31
|
-
"check": "node --check
|
|
33
|
+
"check": "node --check extensions/pentect.js",
|
|
32
34
|
"test": "node --test"
|
|
33
35
|
},
|
|
34
36
|
"dependencies": {
|
|
35
|
-
"
|
|
36
|
-
"pentect": "0.0.27"
|
|
37
|
+
"pentect": "0.0.29"
|
|
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
|
-
}
|