@anton-kochev/pi-extensions 0.1.0 → 0.2.0

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,32 +1,22 @@
1
1
  # pi-extensions
2
2
 
3
- Pi extensions for personal use, installed as a single pi-package from this repository.
3
+ [![npm version](https://img.shields.io/npm/v/@anton-kochev/pi-extensions.svg)](https://www.npmjs.com/package/@anton-kochev/pi-extensions)
4
4
 
5
- ## Install via pithos
5
+ Pi extensions for personal use.
6
6
 
7
- In your project's `.pithos`:
8
-
9
- ```yaml
10
- pi:
11
- extensions:
12
- squiggle: "git:https://github.com/anton-kochev/pi-extensions.git#main"
13
- ```
14
-
15
- Pithos's entrypoint passes this to `pi install`, which clones the repo, runs `npm install`, and registers the extensions declared in the root `pi.extensions` manifest.
16
-
17
- ## Install directly with pi
7
+ ## Install
18
8
 
19
9
  ```bash
20
- pi install git:github.com/anton-kochev/pi-extensions
10
+ pi install npm:@anton-kochev/pi-extensions
21
11
  ```
22
12
 
23
- Pin to a tag for reproducibility:
13
+ Or pin to a specific version:
24
14
 
25
15
  ```bash
26
- pi install git:github.com/anton-kochev/pi-extensions@v0.1.0
16
+ pi install npm:@anton-kochev/pi-extensions@<version>
27
17
  ```
28
18
 
29
- ## Extensions in this repo
19
+ ## Extensions
30
20
 
31
21
  - [`squiggle/`](./squiggle) — quietly polish grammar and spelling in user prompts.
32
22
 
@@ -39,3 +29,12 @@ pi install -l ./squiggle
39
29
  ```
40
30
 
41
31
  Each subdirectory has its own `package.json` so individual extensions remain installable in isolation.
32
+
33
+ ## Release
34
+
35
+ ```bash
36
+ npm version patch # or minor/major
37
+ git push --follow-tags
38
+ ```
39
+
40
+ Trusted publishing handles the rest — the workflow at `.github/workflows/publish.yml` fires on tag push and publishes to npm via OIDC.
package/package.json CHANGED
@@ -1,11 +1,20 @@
1
1
  {
2
2
  "name": "@anton-kochev/pi-extensions",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Pi extensions.",
5
- "keywords": ["pi-package"],
5
+ "keywords": [
6
+ "pi-package"
7
+ ],
6
8
  "license": "MIT",
7
9
  "type": "module",
8
- "files": ["squiggle", "README.md"],
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/anton-kochev/pi-extensions.git"
13
+ },
14
+ "files": [
15
+ "squiggle",
16
+ "README.md"
17
+ ],
9
18
  "publishConfig": {
10
19
  "access": "public"
11
20
  },
@@ -14,6 +23,8 @@
14
23
  "@earendil-works/pi-coding-agent": "*"
15
24
  },
16
25
  "pi": {
17
- "extensions": ["./squiggle/extensions"]
26
+ "extensions": [
27
+ "./squiggle/extensions"
28
+ ]
18
29
  }
19
30
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  Quietly polish grammar and spelling in your pi prompts.
4
4
 
5
- The extension intercepts user input, corrects spelling and grammar using a configured model, shows a colored diff, and submits the corrected prompt automatically without confirmation. Named after the red squiggle from your favorite spell-checker.
5
+ The extension intercepts user input, shows a `squiggling...` spinner while processing, corrects spelling and grammar using a configured model, shows a colored diff, and submits the corrected prompt automatically without confirmation. Named after the red squiggle from your favorite spell-checker.
6
6
 
7
7
  ## Install
8
8
 
@@ -68,14 +68,17 @@ SQUIGGLE_MODEL=openai-codex/gpt-5.4-mini pi
68
68
  SQUIGGLE_MAX_CHARS=1000 pi
69
69
  ```
70
70
 
71
- ## Status
71
+ ## Commands
72
72
 
73
73
  Inside pi:
74
74
 
75
75
  ```text
76
- /squiggle-status
76
+ /squiggle toggle # switch between on/off
77
+ /squiggle-status # show status
77
78
  ```
78
79
 
80
+ The toggle state is saved in the current pi session and overrides `.pi/squiggle.json` and environment configuration for that session.
81
+
79
82
  ## Notes
80
83
 
81
84
  This package imports pi runtime packages as peer dependencies:
@@ -4,23 +4,44 @@ import { complete, type UserMessage } from "@earendil-works/pi-ai";
4
4
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
 
6
6
  export default function squiggle(pi: ExtensionAPI) {
7
+ let runtimeMode: SquiggleConfig["mode"] | undefined;
8
+
9
+ pi.on("session_start", async (_event, ctx) => {
10
+ runtimeMode = restoreRuntimeMode(ctx);
11
+ });
12
+
13
+ pi.registerCommand("squiggle", {
14
+ description: "Toggle squiggle on/off",
15
+ handler: async (args, ctx) => {
16
+ const command = args.trim().toLowerCase();
17
+ if (command !== "toggle") {
18
+ ctx.ui.notify("Usage: /squiggle toggle", "warning");
19
+ return;
20
+ }
21
+
22
+ const config = loadEffectiveConfig(ctx.cwd, runtimeMode);
23
+ runtimeMode = config.mode === "on" ? "off" : "on";
24
+ persistRuntimeMode(pi, runtimeMode);
25
+ ctx.ui.notify(formatStatus(ctx, loadEffectiveConfig(ctx.cwd, runtimeMode)), "info");
26
+ },
27
+ });
28
+
7
29
  pi.registerCommand("squiggle-status", {
8
30
  description: "Show whether squiggle is loaded",
9
31
  handler: async (_args, ctx) => {
10
- const config = loadConfig(ctx.cwd);
11
- const model = selectCorrectionModel(ctx, config);
12
- ctx.ui.notify(`squiggle is loaded (${config.mode}, ${formatModel(model)}).`, "info");
32
+ ctx.ui.notify(formatStatus(ctx, loadEffectiveConfig(ctx.cwd, runtimeMode)), "info");
13
33
  },
14
34
  });
15
35
 
16
36
  pi.on("input", async (event, ctx) => {
17
37
  if (event.source === "extension") return { action: "continue" };
18
38
 
19
- const config = loadConfig(ctx.cwd);
39
+ const config = loadEffectiveConfig(ctx.cwd, runtimeMode);
20
40
  if (config.mode === "off") return { action: "continue" };
21
41
  if (!event.text.trim()) return { action: "continue" };
22
42
 
23
- const corrected = await correctWithModel(event.text, ctx, config);
43
+ const stopIndicator = startSquiggleIndicator(ctx);
44
+ const corrected = await correctWithModel(event.text, ctx, config).finally(stopIndicator);
24
45
  if (!corrected || corrected === event.text) return { action: "continue" };
25
46
 
26
47
  if (ctx.hasUI) ctx.ui.notify(formatColoredDiff(event.text, corrected), "info");
@@ -100,6 +121,28 @@ function loadConfig(cwd: string): SquiggleConfig {
100
121
  };
101
122
  }
102
123
 
124
+ function loadEffectiveConfig(cwd: string, runtimeMode: SquiggleConfig["mode"] | undefined): SquiggleConfig {
125
+ const config = loadConfig(cwd);
126
+ return { ...config, mode: runtimeMode ?? config.mode };
127
+ }
128
+
129
+ function restoreRuntimeMode(ctx: ExtensionContext): SquiggleConfig["mode"] | undefined {
130
+ for (const entry of [...ctx.sessionManager.getEntries()].reverse()) {
131
+ if (entry.type !== "custom" || entry.customType !== "squiggle-mode") continue;
132
+ const data = (entry as { data?: { mode?: unknown } }).data;
133
+ return normalizeMode(data?.mode);
134
+ }
135
+ return undefined;
136
+ }
137
+
138
+ function persistRuntimeMode(pi: ExtensionAPI, mode: SquiggleConfig["mode"]): void {
139
+ pi.appendEntry("squiggle-mode", { mode });
140
+ }
141
+
142
+ function formatStatus(ctx: ExtensionContext, config: SquiggleConfig): string {
143
+ return `squiggle is ${config.mode} (${formatModel(selectCorrectionModel(ctx, config))}).`;
144
+ }
145
+
103
146
  function readConfigFile(cwd: string): Partial<SquiggleConfig> {
104
147
  const path = join(cwd, ".pi", "squiggle.json");
105
148
  if (!existsSync(path)) return {};
@@ -143,27 +186,46 @@ function formatModel(model: ReturnType<typeof selectCorrectionModel>): string {
143
186
  return model ? `${model.provider}/${model.id}` : "no model";
144
187
  }
145
188
 
189
+ function startSquiggleIndicator(ctx: ExtensionContext): () => void {
190
+ if (!ctx.hasUI) return () => {};
191
+
192
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
193
+ let frame = 0;
194
+ let timer: ReturnType<typeof setInterval> | undefined;
195
+
196
+ const render = () => {
197
+ const theme = ctx.ui.theme;
198
+ ctx.ui.setStatus("squiggle", theme.fg("accent", frames[frame]!) + theme.fg("dim", " squiggling..."));
199
+ frame = (frame + 1) % frames.length;
200
+ };
201
+
202
+ render();
203
+ timer = setInterval(render, 120);
204
+
205
+ return () => {
206
+ if (timer) clearInterval(timer);
207
+ ctx.ui.setStatus("squiggle", undefined);
208
+ };
209
+ }
210
+
146
211
  type DiffOp = {
147
212
  type: "same" | "add" | "remove";
148
213
  text: string;
149
214
  };
150
215
 
151
216
  function formatColoredDiff(before: string, after: string): string {
152
- const dim = "\x1b[90;3m";
153
217
  const same = "\x1b[90;3m";
154
218
  const added = "\x1b[32;3m";
155
219
  const removed = "\x1b[31;3m";
156
220
  const reset = "\x1b[0m";
157
221
 
158
- const rendered = diffChars(before.trim(), after.trim())
222
+ return diffChars(before.trim(), after.trim())
159
223
  .map((op) => {
160
224
  if (op.type === "add") return `${added}${op.text}${reset}`;
161
225
  if (op.type === "remove") return `${removed}${op.text}${reset}`;
162
226
  return `${same}${op.text}${reset}`;
163
227
  })
164
228
  .join("");
165
-
166
- return `${dim}squiggle:${reset}\n${rendered}`;
167
229
  }
168
230
 
169
231
  function diffChars(before: string, after: string): DiffOp[] {