pi-image-tools 1.3.1 → 1.4.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/CHANGELOG.md CHANGED
@@ -1,123 +1,135 @@
1
- # Changelog
2
-
3
- ## [Unreleased]
4
-
5
- ## [1.3.1] - 2026-06-16
6
-
7
- ### Fixed
8
- - Default paste-image shortcuts now avoid Pi built-in shortcut conflicts on all platforms, including macOS. Previously macOS defaulted to overriding Pi's built-in `ctrl+v` image paste binding and printed a startup conflict warning. (reported by @gaodes in [#8](https://github.com/MasuRii/pi-image-tools/issues/8))
9
- - Transcode clipboard images that arrive in MIME types model providers reject (most notably `image/bmp` from WSLg's Wayland clipboard bridge for Windows clipboard images, and `image/tiff` from some macOS sources) into PNG via ImageMagick before attaching them. Previously, pasting a Windows screenshot inside WSL produced an `Unknown image type: image/bmp` error from the model provider that blocked the message from being sent. (contributed by @codeviking428 in [#9](https://github.com/MasuRii/pi-image-tools/pull/9))
10
-
11
- ## [1.3.0] - 2026-06-01
12
-
13
- ### Added
14
- - Added a provider registry for platform-specific clipboard readers.
15
- - Added macOS `pngpaste` and `osascript` clipboard fallbacks.
16
- - Added an inline preview queue and chat component for user image previews.
17
-
18
- ### Changed
19
- - Deferred preview, clipboard, and recent-image module loading from the extension entrypoint.
20
- - Made shortcut conflict defaults platform-aware.
21
- - Updated Pi peer dependency ranges and the optional clipboard dependency for 0.78-compatible runtimes.
22
-
23
- ### Fixed
24
- - Improved shell and session handling for clipboard command providers.
25
-
26
- ## [1.2.1] - 2026-05-26
27
-
28
- ### Changed
29
- - Widened `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui` peer dependency ranges to `^0.74.0 || ^0.75.0`.
30
-
31
- ## [1.2.0] - 2026-05-22
32
-
33
- ### Added
34
- - Added Linux Sixel previews through `img2sixel` detection and conversion when the converter is available on `PATH`.
35
- - Added asynchronous image preview conversion so user-message previews can fall back without blocking the TUI.
36
-
37
- ### Changed
38
- - Updated Pi peer dependencies and runtime imports to the `@earendil-works` scope.
39
- - Improved preview setup diagnostics so missing Sixel converters report actionable fallback warnings.
40
-
41
- ### Fixed
42
- - Hardened debug logging with redaction and asynchronous file writes while keeping logging disabled by default.
43
-
44
- ## [1.1.0] - 2026-05-03
45
-
46
- ### Added
47
- - Added centralized image MIME, image-size, PowerShell, and error utilities used by clipboard, preview, attachment, and recent-image flows.
48
- - Added size-limit enforcement for image attachments, recent-cache writes, recent-image loads, and Sixel preview conversion through `PI_IMAGE_TOOLS_MAX_IMAGE_BYTES`.
49
-
50
- ### Changed
51
- - Reworked recent-image caching to use an extension-owned cache directory with safe pruning that preserves user files.
52
- - Reworked Sixel preview rendering to use an existing PowerShell `Sixel` module only, normalize converter output into complete terminal sequences, and fall back to native previews with actionable warnings.
53
- - Reworked terminal image width resolution to honor Pi project/global `terminal.imageWidthCells` settings with a documented default fallback.
54
-
55
- ### Fixed
56
- - Prevented extension debug logging from writing terminal output; debug events now remain file-based and disabled by default.
57
- - Preserved Sixel, Kitty, and iTerm inline image protocol rows during preview width fitting.
58
-
59
- ## [1.0.11] - 2026-04-25
60
-
61
- ### Changed
62
- - 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).
63
- - Replaced the placeholder `enabled` config with validated `debug`, explicit `shortcuts.pasteImage`, configurable built-in conflict avoidance, and built-in warning suppression settings.
64
- - Removed obsolete packaged README image assets now that the README uses a GitHub-hosted image.
65
-
66
- ## [1.0.9] - 2026-04-01
67
-
68
- ### Changed
69
- - Updated README image to use HTML tag for better npm display compatibility
70
- - Added npm keywords for improved package discoverability
71
- - Added Related Pi Extensions cross-linking section to README
72
-
73
- ## [1.0.8] - 2026-04-01
74
-
75
- ### Changed
76
- - Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.64.0
77
-
78
- ## [1.0.7] - 2026-03-23
79
-
80
- ### Changed
81
- - Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.62.0
82
-
83
- ## [1.0.6] - 2026-03-12
84
-
85
- ### Changed
86
- - Updated AWS SDK client-bedrock-runtime to 3.1005.0
87
-
88
- ## [1.0.5] - 2026-03-12
89
-
90
- ### Changed
91
- - Updated AWS SDK client-bedrock-runtime to 3.1005.0
92
-
93
- ## [1.0.4] - 2026-03-07
94
-
95
- ### Added
96
- - Added Linux clipboard image support via `wl-paste` and `xclip` fallback readers.
97
- - Added Linux and macOS default recent-image discovery locations so the recent picker works beyond Windows.
98
- - Added non-Windows image paste shortcuts including `Ctrl+V` in addition to the existing alternate bindings.
99
-
100
- ### Changed
101
- - Updated README documentation to reflect current cross-platform support, clipboard backends, recent-image discovery behavior, and inline preview details.
102
-
103
- ### Fixed
104
- - Removed the Windows-only extension gate so supported non-Windows platforms can register commands and shortcuts.
105
- - Preserved Kitty and iTerm inline image protocol rows during preview width fitting, alongside the existing Sixel-safe handling.
106
- - Improved clipboard reader error handling so unsupported environments report missing backends more clearly.
107
-
108
- ## [1.0.3] - 2026-03-04
109
-
110
- ### Fixed
111
- - Use absolute GitHub raw URL for README image to fix npm display
112
-
113
- ## [1.0.2] - 2026-03-04
114
-
115
- ### Changed
116
- - Rewrote README.md with professional documentation standards
117
- - Added comprehensive feature documentation, configuration reference, and usage examples
118
-
119
- ## 1.0.0
120
-
121
- - Standardized repository layout to `src/` + root shim entrypoint.
122
- - Added TypeScript/Bundler project config, package metadata, and publish whitelist.
123
- - Added standard docs, license, and initial runtime config files.
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [1.4.0] - 2026-07-03
6
+
7
+ ### Added
8
+ - Added an `enabled` config toggle to disable the extension without uninstalling. ([7c176b9](https://github.com/MasuRii/pi-image-tools/commit/7c176b9402e0681f23e91258fde4b5f8139aa14d))
9
+
10
+ ### Changed
11
+ - Widened Pi peer dependency ranges to 0.80 and added security dependency overrides. ([04f6847](https://github.com/MasuRii/pi-image-tools/commit/04f6847f3d29f50c63d5b069293b2dbff2f7bc64))
12
+ - Extracted a shared provider base class and preview logging to reduce duplication. ([7c176b9](https://github.com/MasuRii/pi-image-tools/commit/7c176b9402e0681f23e91258fde4b5f8139aa14d))
13
+
14
+ ### Fixed
15
+ - Restricted clipboard and transcode commands to an allowlist to prevent command injection. ([27c9a0b](https://github.com/MasuRii/pi-image-tools/commit/27c9a0b7f549bb248ff2544243c5f0dc7299b562))
16
+
17
+ ## [1.3.1] - 2026-06-16
18
+
19
+ ### Fixed
20
+ - Default paste-image shortcuts now avoid Pi built-in shortcut conflicts on all platforms, including macOS. Previously macOS defaulted to overriding Pi's built-in `ctrl+v` image paste binding and printed a startup conflict warning. (reported by @gaodes in [#8](https://github.com/MasuRii/pi-image-tools/issues/8))
21
+ - Transcode clipboard images that arrive in MIME types model providers reject (most notably `image/bmp` from WSLg's Wayland clipboard bridge for Windows clipboard images, and `image/tiff` from some macOS sources) into PNG via ImageMagick before attaching them. Previously, pasting a Windows screenshot inside WSL produced an `Unknown image type: image/bmp` error from the model provider that blocked the message from being sent. (contributed by @codeviking428 in [#9](https://github.com/MasuRii/pi-image-tools/pull/9))
22
+
23
+ ## [1.3.0] - 2026-06-01
24
+
25
+ ### Added
26
+ - Added a provider registry for platform-specific clipboard readers.
27
+ - Added macOS `pngpaste` and `osascript` clipboard fallbacks.
28
+ - Added an inline preview queue and chat component for user image previews.
29
+
30
+ ### Changed
31
+ - Deferred preview, clipboard, and recent-image module loading from the extension entrypoint.
32
+ - Made shortcut conflict defaults platform-aware.
33
+ - Updated Pi peer dependency ranges and the optional clipboard dependency for 0.78-compatible runtimes.
34
+
35
+ ### Fixed
36
+ - Improved shell and session handling for clipboard command providers.
37
+
38
+ ## [1.2.1] - 2026-05-26
39
+
40
+ ### Changed
41
+ - Widened `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui` peer dependency ranges to `^0.74.0 || ^0.75.0`.
42
+
43
+ ## [1.2.0] - 2026-05-22
44
+
45
+ ### Added
46
+ - Added Linux Sixel previews through `img2sixel` detection and conversion when the converter is available on `PATH`.
47
+ - Added asynchronous image preview conversion so user-message previews can fall back without blocking the TUI.
48
+
49
+ ### Changed
50
+ - Updated Pi peer dependencies and runtime imports to the `@earendil-works` scope.
51
+ - Improved preview setup diagnostics so missing Sixel converters report actionable fallback warnings.
52
+
53
+ ### Fixed
54
+ - Hardened debug logging with redaction and asynchronous file writes while keeping logging disabled by default.
55
+
56
+ ## [1.1.0] - 2026-05-03
57
+
58
+ ### Added
59
+ - Added centralized image MIME, image-size, PowerShell, and error utilities used by clipboard, preview, attachment, and recent-image flows.
60
+ - Added size-limit enforcement for image attachments, recent-cache writes, recent-image loads, and Sixel preview conversion through `PI_IMAGE_TOOLS_MAX_IMAGE_BYTES`.
61
+
62
+ ### Changed
63
+ - Reworked recent-image caching to use an extension-owned cache directory with safe pruning that preserves user files.
64
+ - Reworked Sixel preview rendering to use an existing PowerShell `Sixel` module only, normalize converter output into complete terminal sequences, and fall back to native previews with actionable warnings.
65
+ - Reworked terminal image width resolution to honor Pi project/global `terminal.imageWidthCells` settings with a documented default fallback.
66
+
67
+ ### Fixed
68
+ - Prevented extension debug logging from writing terminal output; debug events now remain file-based and disabled by default.
69
+ - Preserved Sixel, Kitty, and iTerm inline image protocol rows during preview width fitting.
70
+
71
+ ## [1.0.11] - 2026-04-25
72
+
73
+ ### Changed
74
+ - 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).
75
+ - Replaced the placeholder `enabled` config with validated `debug`, explicit `shortcuts.pasteImage`, configurable built-in conflict avoidance, and built-in warning suppression settings.
76
+ - Removed obsolete packaged README image assets now that the README uses a GitHub-hosted image.
77
+
78
+ ## [1.0.9] - 2026-04-01
79
+
80
+ ### Changed
81
+ - Updated README image to use HTML tag for better npm display compatibility
82
+ - Added npm keywords for improved package discoverability
83
+ - Added Related Pi Extensions cross-linking section to README
84
+
85
+ ## [1.0.8] - 2026-04-01
86
+
87
+ ### Changed
88
+ - Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.64.0
89
+
90
+ ## [1.0.7] - 2026-03-23
91
+
92
+ ### Changed
93
+ - Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.62.0
94
+
95
+ ## [1.0.6] - 2026-03-12
96
+
97
+ ### Changed
98
+ - Updated AWS SDK client-bedrock-runtime to 3.1005.0
99
+
100
+ ## [1.0.5] - 2026-03-12
101
+
102
+ ### Changed
103
+ - Updated AWS SDK client-bedrock-runtime to 3.1005.0
104
+
105
+ ## [1.0.4] - 2026-03-07
106
+
107
+ ### Added
108
+ - Added Linux clipboard image support via `wl-paste` and `xclip` fallback readers.
109
+ - Added Linux and macOS default recent-image discovery locations so the recent picker works beyond Windows.
110
+ - Added non-Windows image paste shortcuts including `Ctrl+V` in addition to the existing alternate bindings.
111
+
112
+ ### Changed
113
+ - Updated README documentation to reflect current cross-platform support, clipboard backends, recent-image discovery behavior, and inline preview details.
114
+
115
+ ### Fixed
116
+ - Removed the Windows-only extension gate so supported non-Windows platforms can register commands and shortcuts.
117
+ - Preserved Kitty and iTerm inline image protocol rows during preview width fitting, alongside the existing Sixel-safe handling.
118
+ - Improved clipboard reader error handling so unsupported environments report missing backends more clearly.
119
+
120
+ ## [1.0.3] - 2026-03-04
121
+
122
+ ### Fixed
123
+ - Use absolute GitHub raw URL for README image to fix npm display
124
+
125
+ ## [1.0.2] - 2026-03-04
126
+
127
+ ### Changed
128
+ - Rewrote README.md with professional documentation standards
129
+ - Added comprehensive feature documentation, configuration reference, and usage examples
130
+
131
+ ## 1.0.0
132
+
133
+ - Standardized repository layout to `src/` + root shim entrypoint.
134
+ - Added TypeScript/Bundler project config, package metadata, and publish whitelist.
135
+ - Added standard docs, license, and initial runtime config files.
package/README.md CHANGED
@@ -1,13 +1,18 @@
1
- # 🖼️ pi-image-tools
1
+ <div align="center">
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)
3
+ # pi-image-tools
4
4
 
5
- Image attachment and preview extension for the **Pi coding agent**.
5
+ [![npm version](https://img.shields.io/npm/v/pi-image-tools?style=for-the-badge)](https://www.npmjs.com/package/pi-image-tools)
6
+ [![License](https://img.shields.io/github/license/MasuRii/pi-image-tools?style=for-the-badge)](LICENSE)
7
+ [![Platform](https://img.shields.io/badge/Platform-macOS%20%7C%20Linux%20%7C%20Windows-blue?style=for-the-badge)]()
6
8
 
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.
9
+ [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Y8Y01PSSVR)
8
10
 
11
+ Image attachment and preview extension for the **Pi coding agent**.
12
+ `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.
9
13
  <img width="1360" height="752" alt="image" src="https://github.com/user-attachments/assets/c7b462c4-6316-495e-a3fa-5a7e22edd91f" />
10
14
 
15
+ </div>
11
16
 
12
17
  ## Features
13
18
 
@@ -349,4 +354,4 @@ npm run check
349
354
 
350
355
  ## License
351
356
 
352
- MIT
357
+ [MIT](LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-image-tools",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Image attachment and rendering extension for Pi TUI",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -20,7 +20,8 @@
20
20
  "build": "npm run typecheck",
21
21
  "lint": "npm run typecheck",
22
22
  "test": "node --test test/*.test.js test/providers/*.test.js",
23
- "check": "npm run build && npm run test"
23
+ "check": "npm run build && npm run test",
24
+ "postinstall": "node -e \"const fs=require('fs'),cp=require('child_process'),p=require('path');const cwd=process.cwd();const normalized=cwd.split(p.sep).join('/');if(!normalized.includes('/.pi/agent/extensions/'))process.exit(0);const s=p.resolve(cwd,'../../scripts/patch-vulnerable-deps.mjs');if(!fs.existsSync(s))process.exit(0);const r=cp.spawnSync(process.execPath,[s,'--target',cwd,'--quiet'],{stdio:'inherit'});process.exit(r.status||0)\""
24
25
  },
25
26
  "keywords": [
26
27
  "pi-package",
@@ -60,10 +61,18 @@
60
61
  ]
61
62
  },
62
63
  "peerDependencies": {
63
- "@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0",
64
- "@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0"
64
+ "@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0 || ^0.80.0",
65
+ "@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0 || ^0.80.0"
65
66
  },
66
67
  "optionalDependencies": {
67
68
  "@mariozechner/clipboard": "^0.3.9"
69
+ },
70
+ "overrides": {
71
+ "protobufjs": "7.6.3",
72
+ "ws": "8.21.0"
73
+ },
74
+ "devDependencies": {
75
+ "@earendil-works/pi-coding-agent": "^0.80.3",
76
+ "@earendil-works/pi-tui": "^0.80.3"
68
77
  }
69
78
  }
package/src/config.ts CHANGED
@@ -14,6 +14,7 @@ export interface ImageToolsShortcutConfig {
14
14
  }
15
15
 
16
16
  export interface ImageToolsConfig {
17
+ enabled: boolean;
17
18
  debug: boolean;
18
19
  shortcuts: ImageToolsShortcutConfig;
19
20
  }
@@ -129,6 +130,7 @@ function parseConfig(rawConfig: unknown, path: string, platform: NodeJS.Platform
129
130
 
130
131
  return {
131
132
  debug: parseBoolean(rawConfig.debug, "debug", path),
133
+ enabled: parseBoolean(rawConfig.enabled, "enabled", path, true),
132
134
  shortcuts: parseShortcutConfig(rawConfig.shortcuts, path, platform),
133
135
  };
134
136
  }
@@ -137,6 +139,7 @@ export function loadImageToolsConfig(path = getConfigPath(), options: LoadImageT
137
139
  const platform = options.platform ?? process.platform;
138
140
  if (!existsSync(path)) {
139
141
  return {
142
+ enabled: true,
140
143
  debug: false,
141
144
  shortcuts: getDefaultShortcutConfig(platform),
142
145
  };
@@ -53,8 +53,10 @@ export class DebugLogger {
53
53
  () => this.appendLine(line),
54
54
  );
55
55
  void this.writeQueue.catch(() => undefined);
56
- } catch {
57
- // Debug logging must never affect extension behavior.
56
+ } catch (error) {
57
+ // Debug logging must never affect extension behavior; the write queue
58
+ // is left untouched so subsequent log calls continue to drain normally.
59
+ void error;
58
60
  }
59
61
  }
60
62
 
@@ -19,6 +19,7 @@ import type { DebugLogger } from "./debug-logger.js";
19
19
  import { getErrorMessage } from "./errors.js";
20
20
  import { getBase64DecodedByteLength, assertImageWithinByteLimit } from "./image-size.js";
21
21
  import { mimeTypeToExtension } from "./image-mime.js";
22
+ import { logPreviewEvent, logPreviewHandlerError } from "./preview-logging.js";
22
23
  import { runBufferedCommand, runPowerShellCommandAsync, type BufferedCommandResult, type PowerShellCommandResult, type RunPowerShellCommandOptions } from "./powershell.js";
23
24
  import { buildSixelRenderLines, ensureCompleteSixelSequence } from "./sixel-protocol.js";
24
25
  import {
@@ -130,14 +131,28 @@ const sixelAvailabilityState: SixelAvailability = {
130
131
  available: false,
131
132
  };
132
133
 
134
+ function resolveSixelAvailabilityState(
135
+ forceRefresh: boolean,
136
+ useCache: boolean,
137
+ ): SixelAvailability {
138
+ return useCache ? sixelAvailabilityState : { checked: false, available: false };
139
+ }
140
+
141
+ function shouldShortCircuitSixelCheck(
142
+ state: SixelAvailability,
143
+ forceRefresh: boolean,
144
+ ): boolean {
145
+ return state.checked && !forceRefresh;
146
+ }
147
+
133
148
  async function ensureSixelModuleAvailable(
134
149
  forceRefresh = false,
135
150
  powerShellRunner: SixelPowerShellRunner = runPowerShellCommandAsync,
136
151
  ): Promise<SixelAvailability> {
137
152
  const useCache = powerShellRunner === runPowerShellCommandAsync;
138
- const state: SixelAvailability = useCache ? sixelAvailabilityState : { checked: false, available: false };
153
+ const state = resolveSixelAvailabilityState(forceRefresh, useCache);
139
154
 
140
- if (state.checked && !forceRefresh) {
155
+ if (shouldShortCircuitSixelCheck(state, forceRefresh)) {
141
156
  return state;
142
157
  }
143
158
 
@@ -185,9 +200,9 @@ async function ensureLinuxSixelConverterAvailable(
185
200
  processRunner: SixelProcessRunner = runBufferedCommand,
186
201
  ): Promise<SixelAvailability> {
187
202
  const useCache = processRunner === runBufferedCommand;
188
- const state: SixelAvailability = useCache ? sixelAvailabilityState : { checked: false, available: false };
203
+ const state = resolveSixelAvailabilityState(forceRefresh, useCache);
189
204
 
190
- if (state.checked && !forceRefresh) {
205
+ if (shouldShortCircuitSixelCheck(state, forceRefresh)) {
191
206
  return state;
192
207
  }
193
208
 
@@ -335,7 +350,9 @@ Write-Output $rendered
335
350
  } finally {
336
351
  try {
337
352
  rmSync(tempBaseDir, { recursive: true, force: true });
338
- } catch {
353
+ } catch (error) {
354
+ // Temp-directory cleanup is best-effort; the OS reclaims leftover dirs.
355
+ void error;
339
356
  }
340
357
  }
341
358
  }
@@ -415,18 +432,6 @@ export type BuildPreviewItemsOptions = TerminalImageWidthOptions & {
415
432
  sixelPowerShellRunner?: SixelPowerShellRunner;
416
433
  };
417
434
 
418
- function logSixelEvent(
419
- logger: DebugLogger | undefined,
420
- event: string,
421
- fields: Record<string, unknown> = {},
422
- ): void {
423
- try {
424
- logger?.log(event, fields);
425
- } catch {
426
- // Debug logging is best-effort and must never block image rendering.
427
- }
428
- }
429
-
430
435
  export async function buildPreviewItems(
431
436
  images: readonly ImagePayload[],
432
437
  options: BuildPreviewItemsOptions = {},
@@ -445,7 +450,7 @@ export async function buildPreviewItems(
445
450
  ? await ensureSixelConverterAvailable(false, platform, processRunner, powerShellRunner)
446
451
  : undefined;
447
452
 
448
- logSixelEvent(options.logger, "image-preview.sixel.detected", {
453
+ logPreviewEvent(options.logger, "image-preview.sixel.detected", {
449
454
  attemptSixel,
450
455
  available: sixelState?.available ?? false,
451
456
  converter: sixelState?.converter ?? null,
@@ -460,7 +465,7 @@ export async function buildPreviewItems(
460
465
  if (attemptSixel && sixelState?.available && sixelState.converter) {
461
466
  const conversion = await convertImageToSixelSequence(image, sixelState.converter, processRunner, powerShellRunner);
462
467
  if (conversion.sequence) {
463
- logSixelEvent(options.logger, "image-preview.sixel.converted", {
468
+ logPreviewEvent(options.logger, "image-preview.sixel.converted", {
464
469
  converter: sixelState.converter,
465
470
  mimeType: image.mimeType,
466
471
  rows,
@@ -476,7 +481,7 @@ export async function buildPreviewItems(
476
481
  continue;
477
482
  }
478
483
 
479
- logSixelEvent(options.logger, "image-preview.sixel.conversion_failed", {
484
+ logPreviewEvent(options.logger, "image-preview.sixel.conversion_failed", {
480
485
  converter: sixelState.converter,
481
486
  mimeType: image.mimeType,
482
487
  error: conversion.error ?? "unknown",
@@ -513,18 +518,6 @@ export interface RegisterImagePreviewDisplayOptions {
513
518
  logger?: DebugLogger;
514
519
  }
515
520
 
516
- function logPreviewHandlerError(
517
- logger: DebugLogger | undefined,
518
- event: string,
519
- error: unknown,
520
- ): void {
521
- try {
522
- logger?.log(event, { error: getErrorMessage(error) });
523
- } catch {
524
- // Debug logging is best-effort inside Pi event handlers.
525
- }
526
- }
527
-
528
521
  export function registerImagePreviewDisplay(
529
522
  pi: ExtensionAPI,
530
523
  options: RegisterImagePreviewDisplayOptions = {},
@@ -5,6 +5,7 @@ import type { ClipboardImage } from "./types.js";
5
5
 
6
6
  const DEFAULT_PRIMARY_TRANSCODE_TOOL = "magick";
7
7
  const LEGACY_UNIX_TRANSCODE_TOOL = "convert";
8
+ const ALLOWED_TRANSCODE_TOOLS = new Set([DEFAULT_PRIMARY_TRANSCODE_TOOL, LEGACY_UNIX_TRANSCODE_TOOL]);
8
9
 
9
10
  /**
10
11
  * Maximum wall-clock time for a single ImageMagick invocation. Bounded so a
@@ -72,6 +73,10 @@ function getDefaultTranscodeTools(platform?: NodeJS.Platform): readonly string[]
72
73
  return [DEFAULT_PRIMARY_TRANSCODE_TOOL, LEGACY_UNIX_TRANSCODE_TOOL];
73
74
  }
74
75
 
76
+ function isAllowedTranscodeTool(tool: string): boolean {
77
+ return ALLOWED_TRANSCODE_TOOLS.has(tool.trim().toLowerCase());
78
+ }
79
+
75
80
  function describeTranscodeTools(tools: readonly string[]): string {
76
81
  const uniqueTools = [...new Set(tools.length > 0 ? tools : [DEFAULT_PRIMARY_TRANSCODE_TOOL])];
77
82
  if (uniqueTools.length === 1) {
@@ -114,7 +119,7 @@ export interface TranscodeRunner {
114
119
  }
115
120
 
116
121
  const defaultTranscodeRunner: TranscodeRunner = (command, args, input) =>
117
- spawnSync(command, args as string[], {
122
+ spawnSync(command, args as string[], { // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process -- command is validated by isAllowedTranscodeTool before the default runner is reached; args are fixed ImageMagick stdin/stdout specs and shell is disabled.
118
123
  input: Buffer.from(input),
119
124
  maxBuffer: TRANSCODE_MAX_BUFFER_BYTES,
120
125
  timeout: TRANSCODE_TIMEOUT_MS,
@@ -167,6 +172,9 @@ export function transcodeToSupportedFormat(
167
172
 
168
173
  const failures: string[] = [];
169
174
  for (const tool of tools) {
175
+ if (!isAllowedTranscodeTool(tool)) {
176
+ throw new Error(`Transcode tool "${tool}" is not an allowed ImageMagick executable. Allowed tools: ${describeTranscodeTools([...ALLOWED_TRANSCODE_TOOLS])}.`);
177
+ }
170
178
  const result = runner(tool, [inputSpec, "png:-"], image.bytes);
171
179
  if (result.error) {
172
180
  failures.push(`${tool}: ${result.error.message}`);
package/src/index.ts CHANGED
@@ -26,39 +26,32 @@ type ImagePreviewModule = typeof import("./image-preview.js");
26
26
  type InlineUserPreviewModule = typeof import("./inline-user-preview.js");
27
27
  type RecentImagesModule = typeof import("./recent-images.js");
28
28
 
29
- let clipboardModulePromise: Promise<ClipboardModule> | undefined;
30
- let imagePreviewModulePromise: Promise<ImagePreviewModule> | undefined;
31
- let inlineUserPreviewModulePromise: Promise<InlineUserPreviewModule> | undefined;
32
- let recentImagesModulePromise: Promise<RecentImagesModule> | undefined;
29
+ const lazyModules = new Map<string, Promise<unknown>>();
33
30
  const imagePreviewDisplayRegistrations = new WeakSet<ExtensionAPI>();
34
31
  const inlineUserPreviewRegistrations = new WeakSet<ExtensionAPI>();
35
32
  const previewRegistrationPromises = new WeakMap<ExtensionAPI, Promise<void>>();
36
33
 
37
- function loadClipboardModule(): Promise<ClipboardModule> {
38
- clipboardModulePromise ??= import("./clipboard.js");
39
- return clipboardModulePromise;
40
- }
41
-
42
- function loadImagePreviewModule(): Promise<ImagePreviewModule> {
43
- imagePreviewModulePromise ??= import("./image-preview.js");
44
- return imagePreviewModulePromise;
45
- }
46
-
47
- function loadInlineUserPreviewModule(): Promise<InlineUserPreviewModule> {
48
- inlineUserPreviewModulePromise ??= import("./inline-user-preview.js");
49
- return inlineUserPreviewModulePromise;
50
- }
34
+ /**
35
+ * Lazily import and cache a module by key. The factory uses a literal
36
+ * `import("./path.js")` so TypeScript keeps full module-type inference; the
37
+ * key retains a single in-flight promise per module across calls.
38
+ */
39
+ function loadModule<T>(key: string, factory: () => Promise<T>): Promise<T> {
40
+ const cached = lazyModules.get(key);
41
+ if (cached !== undefined) {
42
+ return cached as Promise<T>;
43
+ }
51
44
 
52
- function loadRecentImagesModule(): Promise<RecentImagesModule> {
53
- recentImagesModulePromise ??= import("./recent-images.js");
54
- return recentImagesModulePromise;
45
+ const promise = factory();
46
+ lazyModules.set(key, promise);
47
+ return promise;
55
48
  }
56
49
 
57
50
  async function ensureImagePreviewDisplayRegistered(
58
51
  pi: ExtensionAPI,
59
52
  logger: DebugLogger,
60
53
  ): Promise<ImagePreviewModule> {
61
- const module = await loadImagePreviewModule();
54
+ const module = await loadModule<ImagePreviewModule>("image-preview", () => import("./image-preview.js"));
62
55
  if (!imagePreviewDisplayRegistrations.has(pi)) {
63
56
  imagePreviewDisplayRegistrations.add(pi);
64
57
  module.registerImagePreviewDisplay(pi, { logger });
@@ -68,12 +61,12 @@ async function ensureImagePreviewDisplayRegistered(
68
61
 
69
62
  async function ensurePreviewRegistrations(pi: ExtensionAPI, logger: DebugLogger): Promise<void> {
70
63
  let registrationPromise = previewRegistrationPromises.get(pi);
71
- if (!registrationPromise) {
64
+ if (registrationPromise === undefined) {
72
65
  registrationPromise = (async () => {
73
66
  await ensureImagePreviewDisplayRegistered(pi, logger);
74
67
 
75
68
  if (!inlineUserPreviewRegistrations.has(pi)) {
76
- const module = await loadInlineUserPreviewModule();
69
+ const module = await loadModule<InlineUserPreviewModule>("inline-user-preview", () => import("./inline-user-preview.js"));
77
70
  inlineUserPreviewRegistrations.add(pi);
78
71
  module.registerInlineUserImagePreview(pi, { logger });
79
72
  }
@@ -116,7 +109,7 @@ function removeAttachmentIndicators(text: string): string {
116
109
 
117
110
  async function cacheImageForRecentPicker(ctx: PasteContext, image: ClipboardImage): Promise<void> {
118
111
  try {
119
- const { persistImageToRecentCache } = await loadRecentImagesModule();
112
+ const { persistImageToRecentCache } = await loadModule<RecentImagesModule>("recent-images", () => import("./recent-images.js"));
120
113
  persistImageToRecentCache(image);
121
114
  } catch (error) {
122
115
  if (ctx.hasUI) {
@@ -206,6 +199,9 @@ async function showRecentSelectionPreview(
206
199
 
207
200
  export default function imageToolsExtension(pi: ExtensionAPI): void {
208
201
  const config = loadImageToolsConfig();
202
+ if (!config.enabled) {
203
+ return;
204
+ }
209
205
  const logger = DebugLogger.create(config);
210
206
  const pendingImages: PendingImage[] = [];
211
207
 
@@ -230,7 +226,7 @@ export default function imageToolsExtension(pi: ExtensionAPI): void {
230
226
  }
231
227
 
232
228
  try {
233
- const { readClipboardImage } = await loadClipboardModule();
229
+ const { readClipboardImage } = await loadModule<ClipboardModule>("clipboard", () => import("./clipboard.js"));
234
230
  const image = await readClipboardImage();
235
231
  if (!image) {
236
232
  ctx.ui.notify("No image found in clipboard.", "warning");
@@ -261,7 +257,7 @@ export default function imageToolsExtension(pi: ExtensionAPI): void {
261
257
  formatRecentImageLabel,
262
258
  getRecentImageCacheDirectory,
263
259
  loadRecentImage,
264
- } = await loadRecentImagesModule();
260
+ } = await loadModule<RecentImagesModule>("recent-images", () => import("./recent-images.js"));
265
261
  const discovery = discoverRecentImages();
266
262
  if (discovery.candidates.length === 0) {
267
263
  ctx.ui.notify(