pi-image-tools 1.0.9 → 1.0.11

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.11] - 2026-04-25
4
+
5
+ ### Changed
6
+ - Avoid Pi's built-in image paste shortcut by default while preserving the previous primary shortcut when users disable or rebind `app.clipboard.pasteImage` (thanks to @danielcherubini for reporting this in PR #3).
7
+ - Replaced the placeholder `enabled` config with validated `debug`, explicit `shortcuts.pasteImage`, configurable built-in conflict avoidance, and built-in warning suppression settings.
8
+ - Removed obsolete packaged README image assets now that the README uses a GitHub-hosted image.
9
+
3
10
  ## [1.0.9] - 2026-04-01
4
11
 
5
12
  ### Changed
@@ -53,12 +60,8 @@
53
60
  - Rewrote README.md with professional documentation standards
54
61
  - Added comprehensive feature documentation, configuration reference, and usage examples
55
62
 
56
- ## 1.0.1
57
-
58
- - Included `asset/` in the npm package whitelist so README image assets ship in the tarball.
59
-
60
63
  ## 1.0.0
61
64
 
62
65
  - Standardized repository layout to `src/` + root shim entrypoint.
63
66
  - Added TypeScript/Bundler project config, package metadata, and publish whitelist.
64
- - Added standard docs, license, and config template/runtime placeholder files.
67
+ - Added standard docs, license, and initial runtime config files.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # 🖼️ pi-image-tools
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/pi-image-tools?style=flat-square)](https://www.npmjs.com/package/pi-image-tools) [![License](https://img.shields.io/github/license/MasuRii/pi-image-tools?style=flat-square)](LICENSE)
4
+
3
5
  Image attachment and preview extension for the **Pi coding agent**.
4
6
 
5
7
  `pi-image-tools` lets you attach clipboard images or recent screenshots to your next user message, then preview them inline in the TUI before the message is sent.
@@ -36,7 +38,7 @@ Place this folder in one of these locations:
36
38
 
37
39
  | Scope | Path |
38
40
  |-------|------|
39
- | Global | `~/.pi/agent/extensions/pi-image-tools` |
41
+ | Global default | `~/.pi/agent/extensions/pi-image-tools` (respects `PI_CODING_AGENT_DIR`) |
40
42
  | Project | `.pi/extensions/pi-image-tools` |
41
43
 
42
44
  Pi auto-discovers extensions in those paths.
@@ -93,8 +95,16 @@ Selecting an entry queues that image for your next message.
93
95
 
94
96
  | Platform | Shortcuts |
95
97
  |----------|-----------|
96
- | Windows | `Alt+V`, `Ctrl+Alt+V` |
97
- | Linux / macOS | `Ctrl+V`, `Alt+V`, `Ctrl+Alt+V` |
98
+ | Windows | `Ctrl+Alt+V`; `Alt+V` when Pi's built-in `app.clipboard.pasteImage` shortcut is disabled or rebound |
99
+ | Linux / macOS | `Alt+V`, `Ctrl+Alt+V`; `Ctrl+V` when Pi's built-in `app.clipboard.pasteImage` shortcut is disabled or rebound |
100
+
101
+ Pi's built-in image paste shortcut is not overridden by default. To keep the previous primary shortcut behavior without startup conflict warnings, disable the built-in binding manually in `~/.pi/agent/keybindings.json`:
102
+
103
+ ```json
104
+ {
105
+ "app.clipboard.pasteImage": []
106
+ }
107
+ ```
98
108
 
99
109
  ## Configuration
100
110
 
@@ -116,19 +126,93 @@ $env:PI_IMAGE_TOOLS_RECENT_DIRS = "C:\Users\me\Pictures\Screenshots;D:\Shares\Sc
116
126
  A config file can be placed at:
117
127
 
118
128
  ```text
119
- ~/.pi/agent/extensions/pi-image-tools/config.json
129
+ Default global path: ~/.pi/agent/extensions/pi-image-tools/config.json
130
+ Actual global path: $PI_CODING_AGENT_DIR/extensions/pi-image-tools/config.json when PI_CODING_AGENT_DIR is set
120
131
  ```
121
132
 
122
133
  Starter template:
123
134
 
124
135
  ```json
125
136
  {
126
- "enabled": true
137
+ "debug": false,
138
+ "shortcuts": {
139
+ "pasteImage": ["ctrl+alt+v"],
140
+ "avoidBuiltinConflicts": true,
141
+ "suppressBuiltinConflictWarnings": true
142
+ }
127
143
  }
128
144
  ```
129
145
 
130
146
  See `config/config.example.json` for the same template.
131
147
 
148
+ #### Debug logging
149
+
150
+ Debug logging is disabled by default. Set `debug` to `true` to append debug events to `debug/debug.log` inside the extension directory:
151
+
152
+ ```json
153
+ {
154
+ "debug": true,
155
+ "shortcuts": {
156
+ "pasteImage": ["ctrl+alt+v"],
157
+ "avoidBuiltinConflicts": true,
158
+ "suppressBuiltinConflictWarnings": true
159
+ }
160
+ }
161
+ ```
162
+
163
+ When `debug` is `false` or omitted, no debug log file is opened or written.
164
+
165
+ #### Custom shortcuts
166
+
167
+ Set `shortcuts.pasteImage` to choose the exact shortcuts registered by `pi-image-tools`. The value can be a single shortcut string or an array of shortcut strings. Add multiple entries when you want several key combinations to run the same paste-image action:
168
+
169
+ ```json
170
+ {
171
+ "debug": false,
172
+ "shortcuts": {
173
+ "pasteImage": ["ctrl+alt+v", "alt+v", "ctrl+shift+v"],
174
+ "avoidBuiltinConflicts": true,
175
+ "suppressBuiltinConflictWarnings": true
176
+ }
177
+ }
178
+ ```
179
+
180
+ A single shortcut is also valid:
181
+
182
+ ```json
183
+ {
184
+ "debug": false,
185
+ "shortcuts": {
186
+ "pasteImage": "ctrl+alt+v",
187
+ "avoidBuiltinConflicts": true,
188
+ "suppressBuiltinConflictWarnings": true
189
+ }
190
+ }
191
+ ```
192
+
193
+ Config changes are applied the next time the extension loads, so restart Pi or reload extensions after editing `config.json`.
194
+
195
+ Not every terminal can transmit every key combination to Pi. Triple-modifier shortcuts such as `ctrl+shift+alt+v` may be intercepted by the OS, terminal, shell, SSH, or tmux before Pi can see them. If a configured shortcut does not work, try a simpler shortcut such as `ctrl+alt+v`, `ctrl+shift+v`, `alt+p`, `f8`, or another function key.
196
+
197
+ Keep `shortcuts.avoidBuiltinConflicts` set to `true` to skip configured paste-image shortcuts that overlap any effective Pi built-in shortcut. Keep `shortcuts.suppressBuiltinConflictWarnings` set to `true` when your goal is specifically to remove Pi's startup conflict warning noise. Both options use the same safe mechanism: `pi-image-tools` does not register the overlapping shortcut, so Pi has nothing to warn about. For example, `ctrl+p` is skipped because Pi uses it for built-in model/session actions.
198
+
199
+ If both options are `false`, Pi handles conflicts itself. Non-reserved built-in conflicts may be taken over by `pi-image-tools`, but Pi can still print a startup warning. Reserved built-in shortcuts cannot be stolen through the extension shortcut API; Pi skips those registrations before `pi-image-tools` can handle them. To use a reserved Pi shortcut, rebind or disable the relevant built-in action in `~/.pi/agent/keybindings.json`, then restart Pi or reload extensions.
200
+
201
+ Use an empty array to disable the extension's paste-image shortcuts while keeping `/paste-image` available:
202
+
203
+ ```json
204
+ {
205
+ "debug": false,
206
+ "shortcuts": {
207
+ "pasteImage": [],
208
+ "avoidBuiltinConflicts": true,
209
+ "suppressBuiltinConflictWarnings": true
210
+ }
211
+ }
212
+ ```
213
+
214
+ If `shortcuts.pasteImage` is omitted, `pi-image-tools` uses non-conflicting defaults and automatically restores the previous primary shortcut when Pi's built-in `app.clipboard.pasteImage` binding is disabled or rebound.
215
+
132
216
  ## Default recent-image search locations
133
217
 
134
218
  ### Windows
@@ -199,16 +283,16 @@ pi-image-tools/
199
283
  │ ├── index.ts # Extension bootstrap and message flow
200
284
  │ ├── commands.ts # /paste-image command registration
201
285
  │ ├── clipboard.ts # Cross-platform clipboard image reading
286
+ │ ├── config.ts # Runtime config loading and validation
287
+ │ ├── debug-logger.ts # File-based debug logging
202
288
  │ ├── recent-images.ts # Recent image discovery and cache management
203
289
  │ ├── image-preview.ts # Preview building and Sixel/native rendering
204
290
  │ ├── inline-user-preview.ts # Inline preview patching for user messages
205
291
  │ ├── keybindings.ts # Keyboard shortcut registration
206
292
  │ ├── temp-file.ts # Temporary file management and cleanup
207
293
  │ └── types.ts # Shared TypeScript types
208
- ├── config/
209
- └── config.example.json # Starter runtime config
210
- └── asset/
211
- └── pi-image-tools.png # README preview image
294
+ └── config/
295
+ └── config.example.json # Starter runtime config
212
296
  ```
213
297
 
214
298
  ## Troubleshooting
@@ -1,3 +1,8 @@
1
- {
2
- "enabled": true
3
- }
1
+ {
2
+ "debug": false,
3
+ "shortcuts": {
4
+ "pasteImage": ["ctrl+alt+v"],
5
+ "avoidBuiltinConflicts": true,
6
+ "suppressBuiltinConflictWarnings": true
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-image-tools",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "Image attachment and rendering extension for Pi TUI",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -11,7 +11,6 @@
11
11
  "index.ts",
12
12
  "src",
13
13
  "config/config.example.json",
14
- "asset",
15
14
  "README.md",
16
15
  "CHANGELOG.md",
17
16
  "LICENSE"
@@ -58,10 +57,10 @@
58
57
  ]
59
58
  },
60
59
  "peerDependencies": {
61
- "@mariozechner/pi-coding-agent": "^0.64.0",
62
- "@mariozechner/pi-tui": "^0.64.0"
60
+ "@mariozechner/pi-coding-agent": "^0.70.2",
61
+ "@mariozechner/pi-tui": "^0.70.2"
63
62
  },
64
63
  "optionalDependencies": {
65
- "@mariozechner/clipboard": "^0.3.2"
64
+ "@mariozechner/clipboard": "^0.3.3"
66
65
  }
67
66
  }
package/src/config.ts ADDED
@@ -0,0 +1,146 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import type { KeyId } from "@mariozechner/pi-tui";
6
+
7
+ const CONFIG_FILE_NAME = "config.json";
8
+ const EXTENSION_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
9
+
10
+ export interface ImageToolsShortcutConfig {
11
+ avoidBuiltinConflicts: boolean;
12
+ suppressBuiltinConflictWarnings: boolean;
13
+ pasteImage?: KeyId[];
14
+ }
15
+
16
+ export interface ImageToolsConfig {
17
+ debug: boolean;
18
+ shortcuts: ImageToolsShortcutConfig;
19
+ }
20
+
21
+ export function isRecord(value: unknown): value is Record<string, unknown> {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
24
+
25
+ export function getExtensionRoot(): string {
26
+ return EXTENSION_ROOT;
27
+ }
28
+
29
+ export function getConfigPath(): string {
30
+ return join(getExtensionRoot(), CONFIG_FILE_NAME);
31
+ }
32
+
33
+ function formatConfigPath(path: string, property: string): string {
34
+ return `${path}${property.length > 0 ? ` property \"${property}\"` : ""}`;
35
+ }
36
+
37
+ function parseBoolean(value: unknown, property: string, path: string): boolean {
38
+ if (value === undefined) {
39
+ return false;
40
+ }
41
+
42
+ if (typeof value !== "boolean") {
43
+ throw new Error(`Invalid pi-image-tools config at ${formatConfigPath(path, property)}: expected a boolean.`);
44
+ }
45
+
46
+ return value;
47
+ }
48
+
49
+ function normalizeShortcut(value: unknown, property: string, path: string): KeyId {
50
+ if (typeof value !== "string") {
51
+ throw new Error(`Invalid pi-image-tools config at ${formatConfigPath(path, property)}: expected a string shortcut.`);
52
+ }
53
+
54
+ const shortcut = value.trim();
55
+ if (shortcut.length === 0) {
56
+ throw new Error(`Invalid pi-image-tools config at ${formatConfigPath(path, property)}: shortcut cannot be empty.`);
57
+ }
58
+
59
+ return shortcut as KeyId;
60
+ }
61
+
62
+ function parseShortcutList(value: unknown, property: string, path: string): KeyId[] | undefined {
63
+ if (value === undefined) {
64
+ return undefined;
65
+ }
66
+
67
+ const rawShortcuts = Array.isArray(value) ? value : [value];
68
+ const shortcuts: KeyId[] = [];
69
+ const seen = new Set<string>();
70
+
71
+ for (const [index, rawShortcut] of rawShortcuts.entries()) {
72
+ const shortcut = normalizeShortcut(rawShortcut, `${property}[${index}]`, path);
73
+ const normalized = shortcut.toLowerCase();
74
+ if (seen.has(normalized)) {
75
+ continue;
76
+ }
77
+
78
+ seen.add(normalized);
79
+ shortcuts.push(shortcut);
80
+ }
81
+
82
+ return shortcuts;
83
+ }
84
+
85
+ function parseShortcutConfig(value: unknown, path: string): ImageToolsShortcutConfig {
86
+ if (value === undefined) {
87
+ return {
88
+ avoidBuiltinConflicts: false,
89
+ suppressBuiltinConflictWarnings: false,
90
+ };
91
+ }
92
+
93
+ if (!isRecord(value)) {
94
+ throw new Error(`Invalid pi-image-tools config at ${formatConfigPath(path, "shortcuts")}: expected an object.`);
95
+ }
96
+
97
+ const avoidBuiltinConflicts = parseBoolean(
98
+ value.avoidBuiltinConflicts,
99
+ "shortcuts.avoidBuiltinConflicts",
100
+ path,
101
+ );
102
+ const suppressBuiltinConflictWarnings = parseBoolean(
103
+ value.suppressBuiltinConflictWarnings,
104
+ "shortcuts.suppressBuiltinConflictWarnings",
105
+ path,
106
+ );
107
+ const pasteImage = parseShortcutList(value.pasteImage, "shortcuts.pasteImage", path);
108
+
109
+ return pasteImage === undefined
110
+ ? { avoidBuiltinConflicts, suppressBuiltinConflictWarnings }
111
+ : { avoidBuiltinConflicts, suppressBuiltinConflictWarnings, pasteImage };
112
+ }
113
+
114
+ function parseConfig(rawConfig: unknown, path: string): ImageToolsConfig {
115
+ if (!isRecord(rawConfig)) {
116
+ throw new Error(`Invalid pi-image-tools config at ${path}: expected a JSON object.`);
117
+ }
118
+
119
+ return {
120
+ debug: parseBoolean(rawConfig.debug, "debug", path),
121
+ shortcuts: parseShortcutConfig(rawConfig.shortcuts, path),
122
+ };
123
+ }
124
+
125
+ export function loadImageToolsConfig(path = getConfigPath()): ImageToolsConfig {
126
+ if (!existsSync(path)) {
127
+ return {
128
+ debug: false,
129
+ shortcuts: {
130
+ avoidBuiltinConflicts: false,
131
+ suppressBuiltinConflictWarnings: false,
132
+ },
133
+ };
134
+ }
135
+
136
+ try {
137
+ const rawConfig = JSON.parse(readFileSync(path, "utf-8")) as unknown;
138
+ return parseConfig(rawConfig, path);
139
+ } catch (error) {
140
+ if (error instanceof SyntaxError) {
141
+ throw new Error(`Invalid pi-image-tools config at ${path}: ${error.message}`);
142
+ }
143
+
144
+ throw error;
145
+ }
146
+ }
@@ -0,0 +1,48 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { getExtensionRoot, type ImageToolsConfig } from "./config.js";
5
+
6
+ const DEBUG_DIRECTORY_NAME = "debug";
7
+ const DEBUG_LOG_FILE_NAME = "debug.log";
8
+
9
+ type DebugFields = Record<string, unknown>;
10
+
11
+ function getErrorMessage(error: unknown): string {
12
+ if (error instanceof Error && error.message.trim().length > 0) {
13
+ return error.message;
14
+ }
15
+
16
+ return "Unknown error";
17
+ }
18
+
19
+ export class DebugLogger {
20
+ private readonly logPath: string | undefined;
21
+
22
+ private constructor(private readonly enabled: boolean) {
23
+ this.logPath = enabled ? join(getExtensionRoot(), DEBUG_DIRECTORY_NAME, DEBUG_LOG_FILE_NAME) : undefined;
24
+ }
25
+
26
+ static create(config: ImageToolsConfig): DebugLogger {
27
+ return new DebugLogger(config.debug);
28
+ }
29
+
30
+ log(event: string, fields: DebugFields = {}): void {
31
+ if (!this.enabled || !this.logPath) {
32
+ return;
33
+ }
34
+
35
+ const debugDirectory = join(getExtensionRoot(), DEBUG_DIRECTORY_NAME);
36
+
37
+ try {
38
+ mkdirSync(debugDirectory, { recursive: true });
39
+ appendFileSync(
40
+ this.logPath,
41
+ `${JSON.stringify({ timestamp: new Date().toISOString(), event, ...fields })}\n`,
42
+ "utf-8",
43
+ );
44
+ } catch (error) {
45
+ throw new Error(`pi-image-tools debug logging failed at ${this.logPath}: ${getErrorMessage(error)}`);
46
+ }
47
+ }
48
+ }