@likelz/pi-gpt 0.1.0 → 0.1.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
@@ -34,11 +34,27 @@ Edit `~/.pi/agent/extensions/pi-gpt.json` (under `PI_CODING_AGENT_DIR` when over
34
34
  ```json
35
35
  {
36
36
  "enabled": false,
37
- "models": ["openai/gpt-6-astra"]
37
+ "models": ["gpt-6-astra"]
38
38
  }
39
39
  ```
40
40
 
41
- Use the exact `provider/model` identifiers shown by Pi, including any additional slashes in a model ID. The list starts empty; add models whose endpoint supports priority processing. Restart Pi or reload extensions after editing the file.
41
+ Entries support three forms, which can be mixed in the same list. Any matching rule enables fast mode when `enabled` is true:
42
+
43
+ - **Model name** without `/`: case-sensitive, exact match against the final segment of the model identifier. `gpt-6-astra` matches across providers, including `magpie/codex/gpt-6-astra`, and excludes `gpt-6-astra-pro`.
44
+ - **Full identifier** containing `/`: exact match against `provider/model`, including any additional model path segments.
45
+ - **Regex** wrapped in `/.../` (without flags): JavaScript regular expression against the complete identifier. Existing regex rules keep their behavior. They use normal regex search semantics; add `^` and `$` where you need boundaries.
46
+
47
+ | Rule | Matches |
48
+ | ------------------- | -------------------------------------------------------------------------------- |
49
+ | `gpt-6-astra` | The exact final model name under any provider or path prefix |
50
+ | `/[/]gpt-6-astra$/` | `openai/gpt-6-astra`, `openai-codex/gpt-6-astra`, and `magpie/codex/gpt-6-astra` |
51
+ | `/^openai[/]gpt-/` | GPT model IDs under the `openai` provider |
52
+ | `openai/gpt-5.4` | Only this exact identifier |
53
+ | `/.*/` | Every model |
54
+
55
+ `[/]` matches a literal slash, so the first rule requires the exact final model name and excludes `gpt-6-astra-pro`. Backslashes need JSON escaping: use `"/gpt-5\\.4$/"` to match a literal dot. Plain strings remain literal; `*` has regex meaning only inside `/.../` rules. Invalid regex configuration produces a warning and starts with fast mode disabled.
56
+
57
+ The list starts empty; add rules for models whose endpoints support priority processing. Restart Pi or reload extensions after editing the file.
42
58
 
43
59
  - `/fast` toggles and persists `enabled`.
44
60
  - Unlisted models and disabled mode leave requests unchanged.
@@ -56,6 +72,24 @@ This extension requests **priority**, preserving the original behavior. OpenAI a
56
72
 
57
73
  Sources: [Ultrafast API](https://developers.openai.com/api/docs/guides/ultrafast-mode), [Fast API](https://developers.openai.com/api/docs/guides/fast-mode), [Codex speed and eligibility](https://learn.chatgpt.com/docs/agent-configuration/speed), [Codex pricing](https://learn.chatgpt.com/docs/pricing).
58
74
 
75
+ ## Publishing
76
+
77
+ [Publish to npm](https://github.com/ekil1100/pi-gpt/actions/workflows/publish.yml) uses GitHub Actions OIDC. It needs no npm token secret. Node.js 24, npm 11.19.0, and Bun 1.4.2 are configured in the workflow. The `prepublishOnly` script runs checks, tests, and the build before publishing.
78
+
79
+ Configure a GitHub Actions trusted publisher in the [npm package settings](https://www.npmjs.com/package/@likelz/pi-gpt/access):
80
+
81
+ | Field | Value |
82
+ | -------------------- | ------------------------------------------ |
83
+ | Organization or user | `ekil1100` |
84
+ | Repository | `pi-gpt` |
85
+ | Workflow filename | `publish.yml` |
86
+ | Environment name | Leave empty |
87
+ | Allowed actions | Allow direct publishing with `npm publish` |
88
+
89
+ Run the workflow manually to check installation, validation, and tarball contents without uploading. This dry run does not verify npm's OIDC trust configuration; that is exercised by an actual release.
90
+
91
+ To release, update `package.json` to a new stable version, commit and push it, then push a matching tag such as `v0.1.1`. The workflow rejects tags that do not match the package version. Version `0.1.0` is already published and cannot be republished. OIDC publishing automatically includes provenance for this public repository and package.
92
+
59
93
  ## Development
60
94
 
61
95
  Use Bun 1.4.2 and Vite+ 1.0.0. Development was verified on Node.js 24.
package/dist/index.mjs CHANGED
@@ -3,9 +3,20 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
3
3
  import { Effect, Either, Schema } from "effect";
4
4
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  //#region src/state.ts
6
+ const isRegex = (rule) => rule.startsWith("/");
7
+ const isValidRule = (rule) => {
8
+ if (!isRegex(rule)) return /^[^/\s]+(?:\/[^\s]+)?$/.test(rule);
9
+ if (rule.length < 3 || !rule.endsWith("/")) return false;
10
+ try {
11
+ new RegExp(rule.slice(1, -1));
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ };
6
17
  const FastModeState = Schema.Struct({
7
18
  enabled: Schema.Boolean,
8
- models: Schema.Array(Schema.String.pipe(Schema.pattern(/^[^/\s]+\/[^\s]+$/)))
19
+ models: Schema.Array(Schema.String.pipe(Schema.filter(isValidRule, { message: () => "Expected a model name, provider/model identifier, or valid /regex/ rule." })))
9
20
  });
10
21
  const emptyState = () => ({
11
22
  enabled: false,
@@ -13,9 +24,13 @@ const emptyState = () => ({
13
24
  });
14
25
  const decodeState = Schema.decodeUnknownSync(Schema.parseJson(FastModeState));
15
26
  const modelKey = (model) => model ? `${model.provider}/${model.id}` : void 0;
27
+ const matchesModel = (rule, key) => {
28
+ if (isRegex(rule)) return new RegExp(rule.slice(1, -1)).test(key);
29
+ return rule === (rule.includes("/") ? key : key.slice(key.lastIndexOf("/") + 1));
30
+ };
16
31
  const isActive = (state, model) => {
17
32
  const key = modelKey(model);
18
- return state.enabled && key !== void 0 && state.models.includes(key);
33
+ return state.enabled && key !== void 0 && state.models.some((pattern) => matchesModel(pattern, key));
19
34
  };
20
35
  const transformRequest = (state, model, payload) => isActive(state, model) && typeof payload === "object" && payload !== null && !Array.isArray(payload) ? {
21
36
  ...payload,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@likelz/pi-gpt",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "GPT fast mode for Pi with explicit model allowlists.",
5
5
  "keywords": [
6
6
  "gpt",
package/src/state.ts CHANGED
@@ -1,8 +1,27 @@
1
1
  import { Schema } from "effect";
2
2
 
3
+ const isRegex = (rule: string): boolean => rule.startsWith("/");
4
+
5
+ const isValidRule = (rule: string): boolean => {
6
+ if (!isRegex(rule)) return /^[^/\s]+(?:\/[^\s]+)?$/.test(rule);
7
+ if (rule.length < 3 || !rule.endsWith("/")) return false;
8
+ try {
9
+ new RegExp(rule.slice(1, -1));
10
+ return true;
11
+ } catch {
12
+ return false;
13
+ }
14
+ };
15
+
3
16
  export const FastModeState = Schema.Struct({
4
17
  enabled: Schema.Boolean,
5
- models: Schema.Array(Schema.String.pipe(Schema.pattern(/^[^/\s]+\/[^\s]+$/))),
18
+ models: Schema.Array(
19
+ Schema.String.pipe(
20
+ Schema.filter(isValidRule, {
21
+ message: () => "Expected a model name, provider/model identifier, or valid /regex/ rule.",
22
+ }),
23
+ ),
24
+ ),
6
25
  });
7
26
  export type FastModeState = typeof FastModeState.Type;
8
27
  export const emptyState = (): FastModeState => ({ enabled: false, models: [] });
@@ -12,9 +31,16 @@ type Model = { readonly provider: string; readonly id: string } | undefined;
12
31
  export const modelKey = (model: Model): string | undefined =>
13
32
  model ? `${model.provider}/${model.id}` : undefined;
14
33
 
34
+ const matchesModel = (rule: string, key: string): boolean => {
35
+ if (isRegex(rule)) return new RegExp(rule.slice(1, -1)).test(key);
36
+ return rule === (rule.includes("/") ? key : key.slice(key.lastIndexOf("/") + 1));
37
+ };
38
+
15
39
  export const isActive = (state: FastModeState, model: Model): boolean => {
16
40
  const key = modelKey(model);
17
- return state.enabled && key !== undefined && state.models.includes(key);
41
+ return (
42
+ state.enabled && key !== undefined && state.models.some((pattern) => matchesModel(pattern, key))
43
+ );
18
44
  };
19
45
 
20
46
  export const transformRequest = (