@mariozechner/pi-coding-agent 0.48.0 → 0.49.1

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.
Files changed (101) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +29 -2
  3. package/dist/core/agent-session.d.ts +2 -1
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +19 -1
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/compaction/compaction.d.ts +11 -0
  8. package/dist/core/compaction/compaction.d.ts.map +1 -1
  9. package/dist/core/compaction/compaction.js +50 -3
  10. package/dist/core/compaction/compaction.js.map +1 -1
  11. package/dist/core/extensions/index.d.ts +1 -1
  12. package/dist/core/extensions/index.d.ts.map +1 -1
  13. package/dist/core/extensions/index.js.map +1 -1
  14. package/dist/core/extensions/loader.d.ts.map +1 -1
  15. package/dist/core/extensions/loader.js +4 -0
  16. package/dist/core/extensions/loader.js.map +1 -1
  17. package/dist/core/extensions/runner.d.ts +4 -2
  18. package/dist/core/extensions/runner.d.ts.map +1 -1
  19. package/dist/core/extensions/runner.js +56 -24
  20. package/dist/core/extensions/runner.js.map +1 -1
  21. package/dist/core/extensions/types.d.ts +23 -0
  22. package/dist/core/extensions/types.d.ts.map +1 -1
  23. package/dist/core/extensions/types.js.map +1 -1
  24. package/dist/core/model-registry.d.ts +2 -0
  25. package/dist/core/model-registry.d.ts.map +1 -1
  26. package/dist/core/model-registry.js +38 -5
  27. package/dist/core/model-registry.js.map +1 -1
  28. package/dist/core/settings-manager.d.ts +3 -0
  29. package/dist/core/settings-manager.d.ts.map +1 -1
  30. package/dist/core/settings-manager.js +7 -0
  31. package/dist/core/settings-manager.js.map +1 -1
  32. package/dist/core/system-prompt.d.ts.map +1 -1
  33. package/dist/core/system-prompt.js +16 -21
  34. package/dist/core/system-prompt.js.map +1 -1
  35. package/dist/core/tools/path-utils.d.ts +0 -1
  36. package/dist/core/tools/path-utils.d.ts.map +1 -1
  37. package/dist/core/tools/path-utils.js +0 -7
  38. package/dist/core/tools/path-utils.js.map +1 -1
  39. package/dist/core/tools/read.d.ts.map +1 -1
  40. package/dist/core/tools/read.js +2 -12
  41. package/dist/core/tools/read.js.map +1 -1
  42. package/dist/index.d.ts +3 -3
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +2 -2
  45. package/dist/index.js.map +1 -1
  46. package/dist/modes/interactive/components/extension-input.d.ts +5 -2
  47. package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  48. package/dist/modes/interactive/components/extension-input.js +9 -0
  49. package/dist/modes/interactive/components/extension-input.js.map +1 -1
  50. package/dist/modes/interactive/components/login-dialog.d.ts +5 -2
  51. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  52. package/dist/modes/interactive/components/login-dialog.js +9 -0
  53. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  54. package/dist/modes/interactive/components/model-selector.d.ts +5 -2
  55. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  56. package/dist/modes/interactive/components/model-selector.js +10 -1
  57. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  58. package/dist/modes/interactive/components/scoped-models-selector.d.ts +5 -2
  59. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  60. package/dist/modes/interactive/components/scoped-models-selector.js +9 -0
  61. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  62. package/dist/modes/interactive/components/session-selector.d.ts +23 -5
  63. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  64. package/dist/modes/interactive/components/session-selector.js +327 -55
  65. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  66. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  67. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  68. package/dist/modes/interactive/components/settings-selector.js +13 -1
  69. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  70. package/dist/modes/interactive/components/tree-selector.d.ts +5 -2
  71. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  72. package/dist/modes/interactive/components/tree-selector.js +23 -0
  73. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  74. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  75. package/dist/modes/interactive/interactive-mode.js +54 -6
  76. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  77. package/dist/modes/print-mode.d.ts.map +1 -1
  78. package/dist/modes/print-mode.js +16 -0
  79. package/dist/modes/print-mode.js.map +1 -1
  80. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  81. package/dist/modes/rpc/rpc-mode.js +16 -0
  82. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  83. package/dist/utils/image-convert.d.ts.map +1 -1
  84. package/dist/utils/image-convert.js +4 -3
  85. package/dist/utils/image-convert.js.map +1 -1
  86. package/dist/utils/image-resize.d.ts.map +1 -1
  87. package/dist/utils/image-resize.js +2 -2
  88. package/dist/utils/image-resize.js.map +1 -1
  89. package/dist/utils/photon.d.ts +6 -13
  90. package/dist/utils/photon.d.ts.map +1 -1
  91. package/dist/utils/photon.js +99 -29
  92. package/dist/utils/photon.js.map +1 -1
  93. package/docs/extensions.md +67 -1
  94. package/docs/session.md +6 -0
  95. package/docs/tui.md +30 -0
  96. package/examples/extensions/README.md +1 -0
  97. package/examples/extensions/custom-header.ts +2 -1
  98. package/examples/extensions/trigger-compact.ts +40 -0
  99. package/examples/extensions/with-deps/package-lock.json +2 -2
  100. package/examples/extensions/with-deps/package.json +1 -1
  101. package/package.json +5 -5
@@ -1 +1 @@
1
- {"version":3,"file":"image-resize.js","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImageContent } from \"@mariozechner/pi-ai\";\nimport { getPhoton } from \"./photon.js\";\n\nexport interface ImageResizeOptions {\n\tmaxWidth?: number; // Default: 2000\n\tmaxHeight?: number; // Default: 2000\n\tmaxBytes?: number; // Default: 4.5MB (below Anthropic's 5MB limit)\n\tjpegQuality?: number; // Default: 80\n}\n\nexport interface ResizedImage {\n\tdata: string; // base64\n\tmimeType: string;\n\toriginalWidth: number;\n\toriginalHeight: number;\n\twidth: number;\n\theight: number;\n\twasResized: boolean;\n}\n\n// 4.5MB - provides headroom below Anthropic's 5MB limit\nconst DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;\n\nconst DEFAULT_OPTIONS: Required<ImageResizeOptions> = {\n\tmaxWidth: 2000,\n\tmaxHeight: 2000,\n\tmaxBytes: DEFAULT_MAX_BYTES,\n\tjpegQuality: 80,\n};\n\n/** Helper to pick the smaller of two buffers */\nfunction pickSmaller(\n\ta: { buffer: Uint8Array; mimeType: string },\n\tb: { buffer: Uint8Array; mimeType: string },\n): { buffer: Uint8Array; mimeType: string } {\n\treturn a.buffer.length <= b.buffer.length ? a : b;\n}\n\n/**\n * Resize an image to fit within the specified max dimensions and file size.\n * Returns the original image if it already fits within the limits.\n *\n * Uses Photon (Rust/WASM) for image processing. If Photon is not available,\n * returns the original image unchanged.\n *\n * Strategy for staying under maxBytes:\n * 1. First resize to maxWidth/maxHeight\n * 2. Try both PNG and JPEG formats, pick the smaller one\n * 3. If still too large, try JPEG with decreasing quality\n * 4. If still too large, progressively reduce dimensions\n */\nexport async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\tconst inputBuffer = Buffer.from(img.data, \"base64\");\n\n\tconst photon = getPhoton();\n\tif (!photon) {\n\t\t// Photon not available, return original image\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType,\n\t\t\toriginalWidth: 0,\n\t\t\toriginalHeight: 0,\n\t\t\twidth: 0,\n\t\t\theight: 0,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\n\tlet image: ReturnType<typeof photon.PhotonImage.new_from_byteslice> | undefined;\n\ttry {\n\t\timage = photon.PhotonImage.new_from_byteslice(new Uint8Array(inputBuffer));\n\n\t\tconst originalWidth = image.get_width();\n\t\tconst originalHeight = image.get_height();\n\t\tconst format = img.mimeType?.split(\"/\")[1] ?? \"png\";\n\n\t\t// Check if already within all limits (dimensions AND size)\n\t\tconst originalSize = inputBuffer.length;\n\t\tif (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: img.data,\n\t\t\t\tmimeType: img.mimeType ?? `image/${format}`,\n\t\t\t\toriginalWidth,\n\t\t\t\toriginalHeight,\n\t\t\t\twidth: originalWidth,\n\t\t\t\theight: originalHeight,\n\t\t\t\twasResized: false,\n\t\t\t};\n\t\t}\n\n\t\t// Calculate initial dimensions respecting max limits\n\t\tlet targetWidth = originalWidth;\n\t\tlet targetHeight = originalHeight;\n\n\t\tif (targetWidth > opts.maxWidth) {\n\t\t\ttargetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);\n\t\t\ttargetWidth = opts.maxWidth;\n\t\t}\n\t\tif (targetHeight > opts.maxHeight) {\n\t\t\ttargetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);\n\t\t\ttargetHeight = opts.maxHeight;\n\t\t}\n\n\t\t// Helper to resize and encode in both formats, returning the smaller one\n\t\tfunction tryBothFormats(\n\t\t\twidth: number,\n\t\t\theight: number,\n\t\t\tjpegQuality: number,\n\t\t): { buffer: Uint8Array; mimeType: string } {\n\t\t\tconst resized = photon!.resize(image!, width, height, photon!.SamplingFilter.Lanczos3);\n\n\t\t\ttry {\n\t\t\t\tconst pngBuffer = resized.get_bytes();\n\t\t\t\tconst jpegBuffer = resized.get_bytes_jpeg(jpegQuality);\n\n\t\t\t\treturn pickSmaller(\n\t\t\t\t\t{ buffer: pngBuffer, mimeType: \"image/png\" },\n\t\t\t\t\t{ buffer: jpegBuffer, mimeType: \"image/jpeg\" },\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tresized.free();\n\t\t\t}\n\t\t}\n\n\t\t// Try to produce an image under maxBytes\n\t\tconst qualitySteps = [85, 70, 55, 40];\n\t\tconst scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];\n\n\t\tlet best: { buffer: Uint8Array; mimeType: string };\n\t\tlet finalWidth = targetWidth;\n\t\tlet finalHeight = targetHeight;\n\n\t\t// First attempt: resize to target dimensions, try both formats\n\t\tbest = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);\n\n\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\t\tmimeType: best.mimeType,\n\t\t\t\toriginalWidth,\n\t\t\t\toriginalHeight,\n\t\t\t\twidth: finalWidth,\n\t\t\t\theight: finalHeight,\n\t\t\t\twasResized: true,\n\t\t\t};\n\t\t}\n\n\t\t// Still too large - try JPEG with decreasing quality\n\t\tfor (const quality of qualitySteps) {\n\t\t\tbest = tryBothFormats(targetWidth, targetHeight, quality);\n\n\t\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\t\treturn {\n\t\t\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\t\t\tmimeType: best.mimeType,\n\t\t\t\t\toriginalWidth,\n\t\t\t\t\toriginalHeight,\n\t\t\t\t\twidth: finalWidth,\n\t\t\t\t\theight: finalHeight,\n\t\t\t\t\twasResized: true,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Still too large - reduce dimensions progressively\n\t\tfor (const scale of scaleSteps) {\n\t\t\tfinalWidth = Math.round(targetWidth * scale);\n\t\t\tfinalHeight = Math.round(targetHeight * scale);\n\n\t\t\tif (finalWidth < 100 || finalHeight < 100) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tfor (const quality of qualitySteps) {\n\t\t\t\tbest = tryBothFormats(finalWidth, finalHeight, quality);\n\n\t\t\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\t\t\t\tmimeType: best.mimeType,\n\t\t\t\t\t\toriginalWidth,\n\t\t\t\t\t\toriginalHeight,\n\t\t\t\t\t\twidth: finalWidth,\n\t\t\t\t\t\theight: finalHeight,\n\t\t\t\t\t\twasResized: true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Last resort: return smallest version we produced\n\t\treturn {\n\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\tmimeType: best.mimeType,\n\t\t\toriginalWidth,\n\t\t\toriginalHeight,\n\t\t\twidth: finalWidth,\n\t\t\theight: finalHeight,\n\t\t\twasResized: true,\n\t\t};\n\t} catch {\n\t\t// Failed to load image\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType,\n\t\t\toriginalWidth: 0,\n\t\t\toriginalHeight: 0,\n\t\t\twidth: 0,\n\t\t\theight: 0,\n\t\t\twasResized: false,\n\t\t};\n\t} finally {\n\t\tif (image) {\n\t\t\timage.free();\n\t\t}\n\t}\n}\n\n/**\n * Format a dimension note for resized images.\n * This helps the model understand the coordinate mapping.\n */\nexport function formatDimensionNote(result: ResizedImage): string | undefined {\n\tif (!result.wasResized) {\n\t\treturn undefined;\n\t}\n\n\tconst scale = result.originalWidth / result.width;\n\treturn `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${result.height}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`;\n}\n"]}
1
+ {"version":3,"file":"image-resize.js","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImageContent } from \"@mariozechner/pi-ai\";\nimport { loadPhoton } from \"./photon.js\";\n\nexport interface ImageResizeOptions {\n\tmaxWidth?: number; // Default: 2000\n\tmaxHeight?: number; // Default: 2000\n\tmaxBytes?: number; // Default: 4.5MB (below Anthropic's 5MB limit)\n\tjpegQuality?: number; // Default: 80\n}\n\nexport interface ResizedImage {\n\tdata: string; // base64\n\tmimeType: string;\n\toriginalWidth: number;\n\toriginalHeight: number;\n\twidth: number;\n\theight: number;\n\twasResized: boolean;\n}\n\n// 4.5MB - provides headroom below Anthropic's 5MB limit\nconst DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;\n\nconst DEFAULT_OPTIONS: Required<ImageResizeOptions> = {\n\tmaxWidth: 2000,\n\tmaxHeight: 2000,\n\tmaxBytes: DEFAULT_MAX_BYTES,\n\tjpegQuality: 80,\n};\n\n/** Helper to pick the smaller of two buffers */\nfunction pickSmaller(\n\ta: { buffer: Uint8Array; mimeType: string },\n\tb: { buffer: Uint8Array; mimeType: string },\n): { buffer: Uint8Array; mimeType: string } {\n\treturn a.buffer.length <= b.buffer.length ? a : b;\n}\n\n/**\n * Resize an image to fit within the specified max dimensions and file size.\n * Returns the original image if it already fits within the limits.\n *\n * Uses Photon (Rust/WASM) for image processing. If Photon is not available,\n * returns the original image unchanged.\n *\n * Strategy for staying under maxBytes:\n * 1. First resize to maxWidth/maxHeight\n * 2. Try both PNG and JPEG formats, pick the smaller one\n * 3. If still too large, try JPEG with decreasing quality\n * 4. If still too large, progressively reduce dimensions\n */\nexport async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\tconst inputBuffer = Buffer.from(img.data, \"base64\");\n\n\tconst photon = await loadPhoton();\n\tif (!photon) {\n\t\t// Photon not available, return original image\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType,\n\t\t\toriginalWidth: 0,\n\t\t\toriginalHeight: 0,\n\t\t\twidth: 0,\n\t\t\theight: 0,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\n\tlet image: ReturnType<typeof photon.PhotonImage.new_from_byteslice> | undefined;\n\ttry {\n\t\timage = photon.PhotonImage.new_from_byteslice(new Uint8Array(inputBuffer));\n\n\t\tconst originalWidth = image.get_width();\n\t\tconst originalHeight = image.get_height();\n\t\tconst format = img.mimeType?.split(\"/\")[1] ?? \"png\";\n\n\t\t// Check if already within all limits (dimensions AND size)\n\t\tconst originalSize = inputBuffer.length;\n\t\tif (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: img.data,\n\t\t\t\tmimeType: img.mimeType ?? `image/${format}`,\n\t\t\t\toriginalWidth,\n\t\t\t\toriginalHeight,\n\t\t\t\twidth: originalWidth,\n\t\t\t\theight: originalHeight,\n\t\t\t\twasResized: false,\n\t\t\t};\n\t\t}\n\n\t\t// Calculate initial dimensions respecting max limits\n\t\tlet targetWidth = originalWidth;\n\t\tlet targetHeight = originalHeight;\n\n\t\tif (targetWidth > opts.maxWidth) {\n\t\t\ttargetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);\n\t\t\ttargetWidth = opts.maxWidth;\n\t\t}\n\t\tif (targetHeight > opts.maxHeight) {\n\t\t\ttargetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);\n\t\t\ttargetHeight = opts.maxHeight;\n\t\t}\n\n\t\t// Helper to resize and encode in both formats, returning the smaller one\n\t\tfunction tryBothFormats(\n\t\t\twidth: number,\n\t\t\theight: number,\n\t\t\tjpegQuality: number,\n\t\t): { buffer: Uint8Array; mimeType: string } {\n\t\t\tconst resized = photon!.resize(image!, width, height, photon!.SamplingFilter.Lanczos3);\n\n\t\t\ttry {\n\t\t\t\tconst pngBuffer = resized.get_bytes();\n\t\t\t\tconst jpegBuffer = resized.get_bytes_jpeg(jpegQuality);\n\n\t\t\t\treturn pickSmaller(\n\t\t\t\t\t{ buffer: pngBuffer, mimeType: \"image/png\" },\n\t\t\t\t\t{ buffer: jpegBuffer, mimeType: \"image/jpeg\" },\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tresized.free();\n\t\t\t}\n\t\t}\n\n\t\t// Try to produce an image under maxBytes\n\t\tconst qualitySteps = [85, 70, 55, 40];\n\t\tconst scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];\n\n\t\tlet best: { buffer: Uint8Array; mimeType: string };\n\t\tlet finalWidth = targetWidth;\n\t\tlet finalHeight = targetHeight;\n\n\t\t// First attempt: resize to target dimensions, try both formats\n\t\tbest = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);\n\n\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\t\tmimeType: best.mimeType,\n\t\t\t\toriginalWidth,\n\t\t\t\toriginalHeight,\n\t\t\t\twidth: finalWidth,\n\t\t\t\theight: finalHeight,\n\t\t\t\twasResized: true,\n\t\t\t};\n\t\t}\n\n\t\t// Still too large - try JPEG with decreasing quality\n\t\tfor (const quality of qualitySteps) {\n\t\t\tbest = tryBothFormats(targetWidth, targetHeight, quality);\n\n\t\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\t\treturn {\n\t\t\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\t\t\tmimeType: best.mimeType,\n\t\t\t\t\toriginalWidth,\n\t\t\t\t\toriginalHeight,\n\t\t\t\t\twidth: finalWidth,\n\t\t\t\t\theight: finalHeight,\n\t\t\t\t\twasResized: true,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Still too large - reduce dimensions progressively\n\t\tfor (const scale of scaleSteps) {\n\t\t\tfinalWidth = Math.round(targetWidth * scale);\n\t\t\tfinalHeight = Math.round(targetHeight * scale);\n\n\t\t\tif (finalWidth < 100 || finalHeight < 100) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tfor (const quality of qualitySteps) {\n\t\t\t\tbest = tryBothFormats(finalWidth, finalHeight, quality);\n\n\t\t\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\t\t\t\tmimeType: best.mimeType,\n\t\t\t\t\t\toriginalWidth,\n\t\t\t\t\t\toriginalHeight,\n\t\t\t\t\t\twidth: finalWidth,\n\t\t\t\t\t\theight: finalHeight,\n\t\t\t\t\t\twasResized: true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Last resort: return smallest version we produced\n\t\treturn {\n\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\tmimeType: best.mimeType,\n\t\t\toriginalWidth,\n\t\t\toriginalHeight,\n\t\t\twidth: finalWidth,\n\t\t\theight: finalHeight,\n\t\t\twasResized: true,\n\t\t};\n\t} catch {\n\t\t// Failed to load image\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType,\n\t\t\toriginalWidth: 0,\n\t\t\toriginalHeight: 0,\n\t\t\twidth: 0,\n\t\t\theight: 0,\n\t\t\twasResized: false,\n\t\t};\n\t} finally {\n\t\tif (image) {\n\t\t\timage.free();\n\t\t}\n\t}\n}\n\n/**\n * Format a dimension note for resized images.\n * This helps the model understand the coordinate mapping.\n */\nexport function formatDimensionNote(result: ResizedImage): string | undefined {\n\tif (!result.wasResized) {\n\t\treturn undefined;\n\t}\n\n\tconst scale = result.originalWidth / result.width;\n\treturn `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${result.height}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`;\n}\n"]}
@@ -8,21 +8,14 @@
8
8
  * The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm')
9
9
  * which bakes the build machine's absolute path into Bun compiled binaries.
10
10
  *
11
- * Solution: Lazy-load photon and gracefully handle failures. Image processing functions
12
- * already have fallbacks that return original images when photon isn't available.
11
+ * Solution:
12
+ * 1. Patch fs.readFileSync to redirect missing photon_rs_bg.wasm reads
13
+ * 2. Copy photon_rs_bg.wasm next to the executable in build:binary
13
14
  */
14
15
  export type { PhotonImage as PhotonImageType } from "@silvia-odwyer/photon-node";
15
16
  /**
16
- * Get the photon module, loading it lazily on first access.
17
- * Returns null if loading fails (e.g., in broken Bun binary).
17
+ * Load the photon module asynchronously.
18
+ * Returns cached module on subsequent calls.
18
19
  */
19
- export declare function getPhoton(): typeof import("@silvia-odwyer/photon-node") | null;
20
- /**
21
- * Check if photon is available and working.
22
- */
23
- export declare function isPhotonAvailable(): boolean;
24
- /**
25
- * Get the error that occurred during photon loading, if any.
26
- */
27
- export declare function getPhotonLoadError(): Error | null;
20
+ export declare function loadPhoton(): Promise<typeof import("@silvia-odwyer/photon-node") | null>;
28
21
  //# sourceMappingURL=photon.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"photon.d.ts","sourceRoot":"","sources":["../../src/utils/photon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,YAAY,EAAE,WAAW,IAAI,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAOjF;;;GAGG;AACH,wBAAgB,SAAS,IAAI,cAAc,4BAA4B,CAAC,GAAG,IAAI,CAiB9E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,KAAK,GAAG,IAAI,CAGjD","sourcesContent":["/**\n * Photon image processing wrapper.\n *\n * This module provides a unified interface to @silvia-odwyer/photon-node that works in:\n * 1. Node.js (development, npm run build)\n * 2. Bun compiled binaries (standalone distribution)\n *\n * The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm')\n * which bakes the build machine's absolute path into Bun compiled binaries.\n *\n * Solution: Lazy-load photon and gracefully handle failures. Image processing functions\n * already have fallbacks that return original images when photon isn't available.\n */\n\n// Re-export types from the main package\nexport type { PhotonImage as PhotonImageType } from \"@silvia-odwyer/photon-node\";\n\n// Lazy-loaded photon module\nlet photonModule: typeof import(\"@silvia-odwyer/photon-node\") | null = null;\nlet loadAttempted = false;\nlet loadError: Error | null = null;\n\n/**\n * Get the photon module, loading it lazily on first access.\n * Returns null if loading fails (e.g., in broken Bun binary).\n */\nexport function getPhoton(): typeof import(\"@silvia-odwyer/photon-node\") | null {\n\tif (loadAttempted) {\n\t\treturn photonModule;\n\t}\n\n\tloadAttempted = true;\n\n\ttry {\n\t\t// Dynamic require to defer loading until actually needed\n\t\t// This also allows the error to be caught gracefully\n\t\tphotonModule = require(\"@silvia-odwyer/photon-node\");\n\t} catch (e) {\n\t\tloadError = e as Error;\n\t\tphotonModule = null;\n\t}\n\n\treturn photonModule;\n}\n\n/**\n * Check if photon is available and working.\n */\nexport function isPhotonAvailable(): boolean {\n\treturn getPhoton() !== null;\n}\n\n/**\n * Get the error that occurred during photon loading, if any.\n */\nexport function getPhotonLoadError(): Error | null {\n\tgetPhoton(); // Ensure load was attempted\n\treturn loadError;\n}\n"]}
1
+ {"version":3,"file":"photon.d.ts","sourceRoot":"","sources":["../../src/utils/photon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAWH,YAAY,EAAE,WAAW,IAAI,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAuFjF;;;GAGG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,cAAc,4BAA4B,CAAC,GAAG,IAAI,CAAC,CAuB9F","sourcesContent":["/**\n * Photon image processing wrapper.\n *\n * This module provides a unified interface to @silvia-odwyer/photon-node that works in:\n * 1. Node.js (development, npm run build)\n * 2. Bun compiled binaries (standalone distribution)\n *\n * The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm')\n * which bakes the build machine's absolute path into Bun compiled binaries.\n *\n * Solution:\n * 1. Patch fs.readFileSync to redirect missing photon_rs_bg.wasm reads\n * 2. Copy photon_rs_bg.wasm next to the executable in build:binary\n */\n\nimport type { PathOrFileDescriptor } from \"fs\";\nimport { createRequire } from \"module\";\nimport * as path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst require = createRequire(import.meta.url);\nconst fs = require(\"fs\") as typeof import(\"fs\");\n\n// Re-export types from the main package\nexport type { PhotonImage as PhotonImageType } from \"@silvia-odwyer/photon-node\";\n\ntype ReadFileSync = typeof fs.readFileSync;\n\nconst WASM_FILENAME = \"photon_rs_bg.wasm\";\n\n// Lazy-loaded photon module\nlet photonModule: typeof import(\"@silvia-odwyer/photon-node\") | null = null;\nlet loadPromise: Promise<typeof import(\"@silvia-odwyer/photon-node\") | null> | null = null;\n\nfunction pathOrNull(file: PathOrFileDescriptor): string | null {\n\tif (typeof file === \"string\") {\n\t\treturn file;\n\t}\n\tif (file instanceof URL) {\n\t\treturn fileURLToPath(file);\n\t}\n\treturn null;\n}\n\nfunction getFallbackWasmPaths(): string[] {\n\tconst execDir = path.dirname(process.execPath);\n\treturn [\n\t\tpath.join(execDir, WASM_FILENAME),\n\t\tpath.join(execDir, \"photon\", WASM_FILENAME),\n\t\tpath.join(process.cwd(), WASM_FILENAME),\n\t];\n}\n\nfunction patchPhotonWasmRead(): () => void {\n\tconst originalReadFileSync: ReadFileSync = fs.readFileSync.bind(fs);\n\tconst fallbackPaths = getFallbackWasmPaths();\n\tconst mutableFs = fs as { readFileSync: ReadFileSync };\n\n\tconst patchedReadFileSync: ReadFileSync = ((...args: Parameters<ReadFileSync>) => {\n\t\tconst [file, options] = args;\n\t\tconst resolvedPath = pathOrNull(file);\n\n\t\tif (resolvedPath?.endsWith(WASM_FILENAME)) {\n\t\t\ttry {\n\t\t\t\treturn originalReadFileSync(...args);\n\t\t\t} catch (error) {\n\t\t\t\tconst err = error as NodeJS.ErrnoException;\n\t\t\t\tif (err?.code && err.code !== \"ENOENT\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\n\t\t\t\tfor (const fallbackPath of fallbackPaths) {\n\t\t\t\t\tif (!fs.existsSync(fallbackPath)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (options === undefined) {\n\t\t\t\t\t\treturn originalReadFileSync(fallbackPath);\n\t\t\t\t\t}\n\t\t\t\t\treturn originalReadFileSync(fallbackPath, options);\n\t\t\t\t}\n\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\treturn originalReadFileSync(...args);\n\t}) as ReadFileSync;\n\n\ttry {\n\t\tmutableFs.readFileSync = patchedReadFileSync;\n\t} catch {\n\t\tObject.defineProperty(fs, \"readFileSync\", {\n\t\t\tvalue: patchedReadFileSync,\n\t\t\twritable: true,\n\t\t\tconfigurable: true,\n\t\t});\n\t}\n\n\treturn () => {\n\t\ttry {\n\t\t\tmutableFs.readFileSync = originalReadFileSync;\n\t\t} catch {\n\t\t\tObject.defineProperty(fs, \"readFileSync\", {\n\t\t\t\tvalue: originalReadFileSync,\n\t\t\t\twritable: true,\n\t\t\t\tconfigurable: true,\n\t\t\t});\n\t\t}\n\t};\n}\n\n/**\n * Load the photon module asynchronously.\n * Returns cached module on subsequent calls.\n */\nexport async function loadPhoton(): Promise<typeof import(\"@silvia-odwyer/photon-node\") | null> {\n\tif (photonModule) {\n\t\treturn photonModule;\n\t}\n\n\tif (loadPromise) {\n\t\treturn loadPromise;\n\t}\n\n\tloadPromise = (async () => {\n\t\tconst restoreReadFileSync = patchPhotonWasmRead();\n\t\ttry {\n\t\t\tphotonModule = await import(\"@silvia-odwyer/photon-node\");\n\t\t\treturn photonModule;\n\t\t} catch {\n\t\t\tphotonModule = null;\n\t\t\treturn photonModule;\n\t\t} finally {\n\t\t\trestoreReadFileSync();\n\t\t}\n\t})();\n\n\treturn loadPromise;\n}\n"]}
@@ -8,44 +8,114 @@
8
8
  * The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm')
9
9
  * which bakes the build machine's absolute path into Bun compiled binaries.
10
10
  *
11
- * Solution: Lazy-load photon and gracefully handle failures. Image processing functions
12
- * already have fallbacks that return original images when photon isn't available.
11
+ * Solution:
12
+ * 1. Patch fs.readFileSync to redirect missing photon_rs_bg.wasm reads
13
+ * 2. Copy photon_rs_bg.wasm next to the executable in build:binary
13
14
  */
15
+ import { createRequire } from "module";
16
+ import * as path from "path";
17
+ import { fileURLToPath } from "url";
18
+ const require = createRequire(import.meta.url);
19
+ const fs = require("fs");
20
+ const WASM_FILENAME = "photon_rs_bg.wasm";
14
21
  // Lazy-loaded photon module
15
22
  let photonModule = null;
16
- let loadAttempted = false;
17
- let loadError = null;
18
- /**
19
- * Get the photon module, loading it lazily on first access.
20
- * Returns null if loading fails (e.g., in broken Bun binary).
21
- */
22
- export function getPhoton() {
23
- if (loadAttempted) {
24
- return photonModule;
23
+ let loadPromise = null;
24
+ function pathOrNull(file) {
25
+ if (typeof file === "string") {
26
+ return file;
25
27
  }
26
- loadAttempted = true;
28
+ if (file instanceof URL) {
29
+ return fileURLToPath(file);
30
+ }
31
+ return null;
32
+ }
33
+ function getFallbackWasmPaths() {
34
+ const execDir = path.dirname(process.execPath);
35
+ return [
36
+ path.join(execDir, WASM_FILENAME),
37
+ path.join(execDir, "photon", WASM_FILENAME),
38
+ path.join(process.cwd(), WASM_FILENAME),
39
+ ];
40
+ }
41
+ function patchPhotonWasmRead() {
42
+ const originalReadFileSync = fs.readFileSync.bind(fs);
43
+ const fallbackPaths = getFallbackWasmPaths();
44
+ const mutableFs = fs;
45
+ const patchedReadFileSync = ((...args) => {
46
+ const [file, options] = args;
47
+ const resolvedPath = pathOrNull(file);
48
+ if (resolvedPath?.endsWith(WASM_FILENAME)) {
49
+ try {
50
+ return originalReadFileSync(...args);
51
+ }
52
+ catch (error) {
53
+ const err = error;
54
+ if (err?.code && err.code !== "ENOENT") {
55
+ throw error;
56
+ }
57
+ for (const fallbackPath of fallbackPaths) {
58
+ if (!fs.existsSync(fallbackPath)) {
59
+ continue;
60
+ }
61
+ if (options === undefined) {
62
+ return originalReadFileSync(fallbackPath);
63
+ }
64
+ return originalReadFileSync(fallbackPath, options);
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
+ return originalReadFileSync(...args);
70
+ });
27
71
  try {
28
- // Dynamic require to defer loading until actually needed
29
- // This also allows the error to be caught gracefully
30
- photonModule = require("@silvia-odwyer/photon-node");
72
+ mutableFs.readFileSync = patchedReadFileSync;
31
73
  }
32
- catch (e) {
33
- loadError = e;
34
- photonModule = null;
74
+ catch {
75
+ Object.defineProperty(fs, "readFileSync", {
76
+ value: patchedReadFileSync,
77
+ writable: true,
78
+ configurable: true,
79
+ });
35
80
  }
36
- return photonModule;
81
+ return () => {
82
+ try {
83
+ mutableFs.readFileSync = originalReadFileSync;
84
+ }
85
+ catch {
86
+ Object.defineProperty(fs, "readFileSync", {
87
+ value: originalReadFileSync,
88
+ writable: true,
89
+ configurable: true,
90
+ });
91
+ }
92
+ };
37
93
  }
38
94
  /**
39
- * Check if photon is available and working.
95
+ * Load the photon module asynchronously.
96
+ * Returns cached module on subsequent calls.
40
97
  */
41
- export function isPhotonAvailable() {
42
- return getPhoton() !== null;
43
- }
44
- /**
45
- * Get the error that occurred during photon loading, if any.
46
- */
47
- export function getPhotonLoadError() {
48
- getPhoton(); // Ensure load was attempted
49
- return loadError;
98
+ export async function loadPhoton() {
99
+ if (photonModule) {
100
+ return photonModule;
101
+ }
102
+ if (loadPromise) {
103
+ return loadPromise;
104
+ }
105
+ loadPromise = (async () => {
106
+ const restoreReadFileSync = patchPhotonWasmRead();
107
+ try {
108
+ photonModule = await import("@silvia-odwyer/photon-node");
109
+ return photonModule;
110
+ }
111
+ catch {
112
+ photonModule = null;
113
+ return photonModule;
114
+ }
115
+ finally {
116
+ restoreReadFileSync();
117
+ }
118
+ })();
119
+ return loadPromise;
50
120
  }
51
121
  //# sourceMappingURL=photon.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"photon.js","sourceRoot":"","sources":["../../src/utils/photon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,4BAA4B;AAC5B,IAAI,YAAY,GAAuD,IAAI,CAAC;AAC5E,IAAI,aAAa,GAAG,KAAK,CAAC;AAC1B,IAAI,SAAS,GAAiB,IAAI,CAAC;AAEnC;;;GAGG;AACH,MAAM,UAAU,SAAS,GAAuD;IAC/E,IAAI,aAAa,EAAE,CAAC;QACnB,OAAO,YAAY,CAAC;IACrB,CAAC;IAED,aAAa,GAAG,IAAI,CAAC;IAErB,IAAI,CAAC;QACJ,yDAAyD;QACzD,qDAAqD;QACrD,YAAY,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;IACtD,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACZ,SAAS,GAAG,CAAU,CAAC;QACvB,YAAY,GAAG,IAAI,CAAC;IACrB,CAAC;IAED,OAAO,YAAY,CAAC;AAAA,CACpB;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,GAAY;IAC5C,OAAO,SAAS,EAAE,KAAK,IAAI,CAAC;AAAA,CAC5B;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,GAAiB;IAClD,SAAS,EAAE,CAAC,CAAC,4BAA4B;IACzC,OAAO,SAAS,CAAC;AAAA,CACjB","sourcesContent":["/**\n * Photon image processing wrapper.\n *\n * This module provides a unified interface to @silvia-odwyer/photon-node that works in:\n * 1. Node.js (development, npm run build)\n * 2. Bun compiled binaries (standalone distribution)\n *\n * The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm')\n * which bakes the build machine's absolute path into Bun compiled binaries.\n *\n * Solution: Lazy-load photon and gracefully handle failures. Image processing functions\n * already have fallbacks that return original images when photon isn't available.\n */\n\n// Re-export types from the main package\nexport type { PhotonImage as PhotonImageType } from \"@silvia-odwyer/photon-node\";\n\n// Lazy-loaded photon module\nlet photonModule: typeof import(\"@silvia-odwyer/photon-node\") | null = null;\nlet loadAttempted = false;\nlet loadError: Error | null = null;\n\n/**\n * Get the photon module, loading it lazily on first access.\n * Returns null if loading fails (e.g., in broken Bun binary).\n */\nexport function getPhoton(): typeof import(\"@silvia-odwyer/photon-node\") | null {\n\tif (loadAttempted) {\n\t\treturn photonModule;\n\t}\n\n\tloadAttempted = true;\n\n\ttry {\n\t\t// Dynamic require to defer loading until actually needed\n\t\t// This also allows the error to be caught gracefully\n\t\tphotonModule = require(\"@silvia-odwyer/photon-node\");\n\t} catch (e) {\n\t\tloadError = e as Error;\n\t\tphotonModule = null;\n\t}\n\n\treturn photonModule;\n}\n\n/**\n * Check if photon is available and working.\n */\nexport function isPhotonAvailable(): boolean {\n\treturn getPhoton() !== null;\n}\n\n/**\n * Get the error that occurred during photon loading, if any.\n */\nexport function getPhotonLoadError(): Error | null {\n\tgetPhoton(); // Ensure load was attempted\n\treturn loadError;\n}\n"]}
1
+ {"version":3,"file":"photon.js","sourceRoot":"","sources":["../../src/utils/photon.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Photon image processing wrapper.\n *\n * This module provides a unified interface to @silvia-odwyer/photon-node that works in:\n * 1. Node.js (development, npm run build)\n * 2. Bun compiled binaries (standalone distribution)\n *\n * The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm')\n * which bakes the build machine's absolute path into Bun compiled binaries.\n *\n * Solution:\n * 1. Patch fs.readFileSync to redirect missing photon_rs_bg.wasm reads\n * 2. Copy photon_rs_bg.wasm next to the executable in build:binary\n */\n\nimport type { PathOrFileDescriptor } from \"fs\";\nimport { createRequire } from \"module\";\nimport * as path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst require = createRequire(import.meta.url);\nconst fs = require(\"fs\") as typeof import(\"fs\");\n\n// Re-export types from the main package\nexport type { PhotonImage as PhotonImageType } from \"@silvia-odwyer/photon-node\";\n\ntype ReadFileSync = typeof fs.readFileSync;\n\nconst WASM_FILENAME = \"photon_rs_bg.wasm\";\n\n// Lazy-loaded photon module\nlet photonModule: typeof import(\"@silvia-odwyer/photon-node\") | null = null;\nlet loadPromise: Promise<typeof import(\"@silvia-odwyer/photon-node\") | null> | null = null;\n\nfunction pathOrNull(file: PathOrFileDescriptor): string | null {\n\tif (typeof file === \"string\") {\n\t\treturn file;\n\t}\n\tif (file instanceof URL) {\n\t\treturn fileURLToPath(file);\n\t}\n\treturn null;\n}\n\nfunction getFallbackWasmPaths(): string[] {\n\tconst execDir = path.dirname(process.execPath);\n\treturn [\n\t\tpath.join(execDir, WASM_FILENAME),\n\t\tpath.join(execDir, \"photon\", WASM_FILENAME),\n\t\tpath.join(process.cwd(), WASM_FILENAME),\n\t];\n}\n\nfunction patchPhotonWasmRead(): () => void {\n\tconst originalReadFileSync: ReadFileSync = fs.readFileSync.bind(fs);\n\tconst fallbackPaths = getFallbackWasmPaths();\n\tconst mutableFs = fs as { readFileSync: ReadFileSync };\n\n\tconst patchedReadFileSync: ReadFileSync = ((...args: Parameters<ReadFileSync>) => {\n\t\tconst [file, options] = args;\n\t\tconst resolvedPath = pathOrNull(file);\n\n\t\tif (resolvedPath?.endsWith(WASM_FILENAME)) {\n\t\t\ttry {\n\t\t\t\treturn originalReadFileSync(...args);\n\t\t\t} catch (error) {\n\t\t\t\tconst err = error as NodeJS.ErrnoException;\n\t\t\t\tif (err?.code && err.code !== \"ENOENT\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\n\t\t\t\tfor (const fallbackPath of fallbackPaths) {\n\t\t\t\t\tif (!fs.existsSync(fallbackPath)) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tif (options === undefined) {\n\t\t\t\t\t\treturn originalReadFileSync(fallbackPath);\n\t\t\t\t\t}\n\t\t\t\t\treturn originalReadFileSync(fallbackPath, options);\n\t\t\t\t}\n\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\treturn originalReadFileSync(...args);\n\t}) as ReadFileSync;\n\n\ttry {\n\t\tmutableFs.readFileSync = patchedReadFileSync;\n\t} catch {\n\t\tObject.defineProperty(fs, \"readFileSync\", {\n\t\t\tvalue: patchedReadFileSync,\n\t\t\twritable: true,\n\t\t\tconfigurable: true,\n\t\t});\n\t}\n\n\treturn () => {\n\t\ttry {\n\t\t\tmutableFs.readFileSync = originalReadFileSync;\n\t\t} catch {\n\t\t\tObject.defineProperty(fs, \"readFileSync\", {\n\t\t\t\tvalue: originalReadFileSync,\n\t\t\t\twritable: true,\n\t\t\t\tconfigurable: true,\n\t\t\t});\n\t\t}\n\t};\n}\n\n/**\n * Load the photon module asynchronously.\n * Returns cached module on subsequent calls.\n */\nexport async function loadPhoton(): Promise<typeof import(\"@silvia-odwyer/photon-node\") | null> {\n\tif (photonModule) {\n\t\treturn photonModule;\n\t}\n\n\tif (loadPromise) {\n\t\treturn loadPromise;\n\t}\n\n\tloadPromise = (async () => {\n\t\tconst restoreReadFileSync = patchPhotonWasmRead();\n\t\ttry {\n\t\t\tphotonModule = await import(\"@silvia-odwyer/photon-node\");\n\t\t\treturn photonModule;\n\t\t} catch {\n\t\t\tphotonModule = null;\n\t\t\treturn photonModule;\n\t\t} finally {\n\t\t\trestoreReadFileSync();\n\t\t}\n\t})();\n\n\treturn loadPromise;\n}\n"]}
@@ -384,7 +384,7 @@ pi.on("session_compact", async (event, ctx) => {
384
384
  });
385
385
  ```
386
386
 
387
- **Examples:** [custom-compaction.ts](../examples/extensions/custom-compaction.ts)
387
+ **Examples:** [custom-compaction.ts](../examples/extensions/custom-compaction.ts), [trigger-compact.ts](../examples/extensions/trigger-compact.ts)
388
388
 
389
389
  #### session_before_tree / session_tree
390
390
 
@@ -677,6 +677,33 @@ pi.on("tool_call", (event, ctx) => {
677
677
  });
678
678
  ```
679
679
 
680
+ ### ctx.getContextUsage()
681
+
682
+ Returns current context usage for the active model. Uses last assistant usage when available, then estimates tokens for trailing messages.
683
+
684
+ ```typescript
685
+ const usage = ctx.getContextUsage();
686
+ if (usage && usage.tokens > 100_000) {
687
+ // ...
688
+ }
689
+ ```
690
+
691
+ ### ctx.compact()
692
+
693
+ Trigger compaction without awaiting completion. Use `onComplete` and `onError` for follow-up actions.
694
+
695
+ ```typescript
696
+ ctx.compact({
697
+ customInstructions: "Focus on recent changes",
698
+ onComplete: (result) => {
699
+ ctx.ui.notify("Compaction completed", "info");
700
+ },
701
+ onError: (error) => {
702
+ ctx.ui.notify(`Compaction failed: ${error.message}`, "error");
703
+ },
704
+ });
705
+ ```
706
+
680
707
  ## ExtensionCommandContext
681
708
 
682
709
  Command handlers receive `ExtensionCommandContext`, which extends `ExtensionContext` with session control methods. These are only available in commands because they can deadlock if called from event handlers.
@@ -877,6 +904,23 @@ if (name) {
877
904
  }
878
905
  ```
879
906
 
907
+ ### pi.setLabel(entryId, label)
908
+
909
+ Set or clear a label on an entry. Labels are user-defined markers for bookmarking and navigation (shown in `/tree` selector).
910
+
911
+ ```typescript
912
+ // Set a label
913
+ pi.setLabel(entryId, "checkpoint-before-refactor");
914
+
915
+ // Clear a label
916
+ pi.setLabel(entryId, undefined);
917
+
918
+ // Read labels via sessionManager
919
+ const label = ctx.sessionManager.getLabel(entryId);
920
+ ```
921
+
922
+ Labels persist in the session and survive restarts. Use them to mark important points (turns, checkpoints) in the conversation tree.
923
+
880
924
  ### pi.registerCommand(name, options)
881
925
 
882
926
  Register a command.
@@ -1276,6 +1320,28 @@ renderResult(result, { expanded, isPartial }, theme) {
1276
1320
  }
1277
1321
  ```
1278
1322
 
1323
+ #### Keybinding Hints
1324
+
1325
+ Use `keyHint()` to display keybinding hints that respect user's keybinding configuration:
1326
+
1327
+ ```typescript
1328
+ import { keyHint } from "@mariozechner/pi-coding-agent";
1329
+
1330
+ renderResult(result, { expanded }, theme) {
1331
+ let text = theme.fg("success", "✓ Done");
1332
+ if (!expanded) {
1333
+ text += ` (${keyHint("expandTools", "to expand")})`;
1334
+ }
1335
+ return new Text(text, 0, 0);
1336
+ }
1337
+ ```
1338
+
1339
+ Available functions:
1340
+ - `keyHint(action, description)` - Editor actions (e.g., `"expandTools"`, `"selectConfirm"`)
1341
+ - `appKeyHint(keybindings, action, description)` - App actions (requires `KeybindingsManager`)
1342
+ - `editorKey(action)` - Get raw key string for editor action
1343
+ - `rawKeyHint(key, description)` - Format a raw key string
1344
+
1279
1345
  #### Best Practices
1280
1346
 
1281
1347
  - Use `Text` with padding `(0, 0)` - the Box handles padding
package/docs/session.md CHANGED
@@ -10,6 +10,12 @@ Sessions are stored as JSONL (JSON Lines) files. Each line is a JSON object with
10
10
 
11
11
  Where `<path>` is the working directory with `/` replaced by `-`.
12
12
 
13
+ ## Deleting Sessions
14
+
15
+ Sessions can be removed by deleting their `.jsonl` files under `~/.pi/agent/sessions/`.
16
+
17
+ Pi also supports deleting sessions interactively from `/resume` (select a session and press `Ctrl+D`, then confirm). When available, pi uses the `trash` CLI to avoid permanent deletion.
18
+
13
19
  ## Session Version
14
20
 
15
21
  Sessions have a version field in the header:
package/docs/tui.md CHANGED
@@ -52,6 +52,36 @@ When a `Focusable` component has focus, TUI:
52
52
 
53
53
  This enables IME candidate windows to appear at the correct position for CJK input methods. The `Editor` and `Input` built-in components already implement this interface.
54
54
 
55
+ ### Container Components with Embedded Inputs
56
+
57
+ When a container component (dialog, selector, etc.) contains an `Input` or `Editor` child, the container must implement `Focusable` and propagate the focus state to the child. Otherwise, the hardware cursor won't be positioned correctly for IME input.
58
+
59
+ ```typescript
60
+ import { Container, type Focusable, Input } from "@mariozechner/pi-tui";
61
+
62
+ class SearchDialog extends Container implements Focusable {
63
+ private searchInput: Input;
64
+
65
+ // Focusable implementation - propagate to child input for IME cursor positioning
66
+ private _focused = false;
67
+ get focused(): boolean {
68
+ return this._focused;
69
+ }
70
+ set focused(value: boolean) {
71
+ this._focused = value;
72
+ this.searchInput.focused = value;
73
+ }
74
+
75
+ constructor() {
76
+ super();
77
+ this.searchInput = new Input();
78
+ this.addChild(this.searchInput);
79
+ }
80
+ }
81
+ ```
82
+
83
+ Without this propagation, typing with an IME (Chinese, Japanese, Korean, etc.) will show the candidate window in the wrong position on screen.
84
+
55
85
  ## Using Components
56
86
 
57
87
  **In hooks** via `ctx.ui.custom()`:
@@ -77,6 +77,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
77
77
  | `pirate.ts` | Demonstrates `systemPromptAppend` to dynamically modify system prompt |
78
78
  | `claude-rules.ts` | Scans `.claude/rules/` folder and lists rules in system prompt |
79
79
  | `custom-compaction.ts` | Custom compaction that summarizes entire conversation |
80
+ | `trigger-compact.ts` | Triggers compaction when context usage exceeds 100k tokens and adds `/trigger-compact` command |
80
81
 
81
82
  ### System Integration
82
83
 
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { ExtensionAPI, Theme } from "@mariozechner/pi-coding-agent";
9
+ import { VERSION } from "@mariozechner/pi-coding-agent";
9
10
 
10
11
  // --- PI MASCOT ---
11
12
  // Based on pi_mascot.ts - the pi agent character
@@ -52,7 +53,7 @@ export default function (pi: ExtensionAPI) {
52
53
  render(_width: number): string[] {
53
54
  const mascotLines = getPiMascot(theme);
54
55
  // Add a subtitle with hint
55
- const subtitle = theme.fg("muted", " shitty coding agent");
56
+ const subtitle = `${theme.fg("muted", " shitty coding agent")}${theme.fg("dim", ` v${VERSION}`)}`;
56
57
  return [...mascotLines, subtitle];
57
58
  },
58
59
  invalidate() {},
@@ -0,0 +1,40 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+
3
+ const COMPACT_THRESHOLD_TOKENS = 100_000;
4
+
5
+ export default function (pi: ExtensionAPI) {
6
+ const triggerCompaction = (ctx: ExtensionContext, customInstructions?: string) => {
7
+ if (ctx.hasUI) {
8
+ ctx.ui.notify("Compaction started", "info");
9
+ }
10
+ ctx.compact({
11
+ customInstructions,
12
+ onComplete: () => {
13
+ if (ctx.hasUI) {
14
+ ctx.ui.notify("Compaction completed", "info");
15
+ }
16
+ },
17
+ onError: (error) => {
18
+ if (ctx.hasUI) {
19
+ ctx.ui.notify(`Compaction failed: ${error.message}`, "error");
20
+ }
21
+ },
22
+ });
23
+ };
24
+
25
+ pi.on("turn_end", (_event, ctx) => {
26
+ const usage = ctx.getContextUsage();
27
+ if (!usage || usage.tokens <= COMPACT_THRESHOLD_TOKENS) {
28
+ return;
29
+ }
30
+ triggerCompaction(ctx);
31
+ });
32
+
33
+ pi.registerCommand("trigger-compact", {
34
+ description: "Trigger compaction immediately",
35
+ handler: async (args, ctx) => {
36
+ const instructions = args.trim() || undefined;
37
+ triggerCompaction(ctx, instructions);
38
+ },
39
+ });
40
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
- "version": "1.12.0",
3
+ "version": "1.13.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-with-deps",
9
- "version": "1.12.0",
9
+ "version": "1.13.1",
10
10
  "dependencies": {
11
11
  "ms": "^2.1.3"
12
12
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
3
  "private": true,
4
- "version": "1.12.0",
4
+ "version": "1.13.1",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mariozechner/pi-coding-agent",
3
- "version": "0.48.0",
3
+ "version": "0.49.1",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -33,16 +33,16 @@
33
33
  "build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets",
34
34
  "build:binary": "npm run build && bun build --compile ./dist/cli.js --outfile dist/pi && npm run copy-binary-assets",
35
35
  "copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",
36
- "copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/",
36
+ "copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
37
37
  "test": "vitest --run",
38
38
  "prepublishOnly": "npm run clean && npm run build"
39
39
  },
40
40
  "dependencies": {
41
41
  "@mariozechner/clipboard": "^0.3.0",
42
42
  "@mariozechner/jiti": "^2.6.2",
43
- "@mariozechner/pi-agent-core": "^0.48.0",
44
- "@mariozechner/pi-ai": "^0.48.0",
45
- "@mariozechner/pi-tui": "^0.48.0",
43
+ "@mariozechner/pi-agent-core": "^0.49.1",
44
+ "@mariozechner/pi-ai": "^0.49.1",
45
+ "@mariozechner/pi-tui": "^0.49.1",
46
46
  "@silvia-odwyer/photon-node": "^0.3.4",
47
47
  "chalk": "^5.5.0",
48
48
  "cli-highlight": "^2.1.11",