@link-assistant/hive-mind 1.73.9 → 1.74.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.
@@ -96,6 +96,103 @@ export const createRawJsonSection = data => {
96
96
  return createCollapsible('📄 Raw JSON', '```json\n' + jsonContent + '\n```');
97
97
  };
98
98
 
99
+ /**
100
+ * Issue #1843: Deep-clone an event object, replacing base64 image payloads with
101
+ * a short `<image data: N base64 chars>` placeholder. Image base64 (Read tool,
102
+ * Playwright screenshots, MCP image results) can be many kilobytes on a single
103
+ * JSON line, which `truncateMiddle` cannot shrink — so without this the "Raw
104
+ * JSON" sections would bloat every image-bearing comment toward the API limit.
105
+ *
106
+ * Redaction is targeted at the three known image carriers and leaves all other
107
+ * fields intact for debugging:
108
+ * - `{ type:'image', source:{ data } }` → source.data
109
+ * - `{ type:'image', data }` → data (MCP shape)
110
+ * - `{ file:{ base64 } }` → file.base64 (Read tool_use_result)
111
+ *
112
+ * @param {*} data
113
+ * @returns {*} a redacted clone (primitives returned as-is)
114
+ */
115
+ export const redactImageData = data => {
116
+ const seen = new WeakSet();
117
+ const placeholder = len => `<image data: ${len} base64 chars>`;
118
+ const walk = node => {
119
+ if (Array.isArray(node)) return node.map(walk);
120
+ if (node && typeof node === 'object') {
121
+ if (seen.has(node)) return '[Circular]';
122
+ seen.add(node);
123
+ const out = {};
124
+ for (const [k, v] of Object.entries(node)) out[k] = walk(v);
125
+ if (out.type === 'image' && out.source && typeof out.source === 'object' && typeof out.source.data === 'string') {
126
+ out.source = { ...out.source, data: placeholder(out.source.data.length) };
127
+ }
128
+ if (out.type === 'image' && typeof out.data === 'string' && out.data.length > 64) {
129
+ out.data = placeholder(out.data.length);
130
+ }
131
+ if (out.file && typeof out.file === 'object' && typeof out.file.base64 === 'string') {
132
+ out.file = { ...out.file, base64: placeholder(out.file.base64.length) };
133
+ }
134
+ return out;
135
+ }
136
+ return node;
137
+ };
138
+ return walk(data);
139
+ };
140
+
141
+ /**
142
+ * Issue #1843: Like createRawJsonSection, but strips base64 image data first.
143
+ * @param {*} data
144
+ * @returns {string}
145
+ */
146
+ export const createRedactedRawJsonSection = data => createRawJsonSection(redactImageData(data));
147
+
148
+ /**
149
+ * Format a byte count as a short human-readable string (e.g. "7.2 MB").
150
+ * @param {number} bytes
151
+ * @returns {string}
152
+ */
153
+ export const formatBytes = bytes => {
154
+ if (typeof bytes !== 'number' || !isFinite(bytes) || bytes < 0) return '';
155
+ if (bytes < 1024) return `${bytes} B`;
156
+ const units = ['KB', 'MB', 'GB'];
157
+ let value = bytes / 1024;
158
+ let i = 0;
159
+ while (value >= 1024 && i < units.length - 1) {
160
+ value /= 1024;
161
+ i++;
162
+ }
163
+ return `${value.toFixed(value >= 10 || Number.isInteger(value) ? 0 : 1)} ${units[i]}`;
164
+ };
165
+
166
+ /**
167
+ * Sanitize Markdown image alt text so it can't break the `![alt](url)` syntax.
168
+ * @param {string} text
169
+ * @returns {string}
170
+ */
171
+ const escapeAltText = text => (!text || typeof text !== 'string' ? 'image' : text.replace(/[[\]\n\r]/g, ' ').trim() || 'image');
172
+
173
+ /**
174
+ * Issue #1843: Render an "🖼️ Images" Markdown section for images surfaced in a
175
+ * tool result. Each entry that has a `url` is embedded inline with `![](url)`;
176
+ * entries without a `url` (upload disabled or failed) degrade to a compact
177
+ * metadata note instead of dumping base64.
178
+ *
179
+ * @param {Array<{ url?: string, mediaType?: string, originalSize?: number, name?: string }>} images
180
+ * @returns {string} Markdown (empty string when there are no images)
181
+ */
182
+ export const formatImageEmbeds = images => {
183
+ if (!Array.isArray(images) || images.length === 0) return '';
184
+ const blocks = images.map((img, i) => {
185
+ const label = img.name || `image ${i + 1}`;
186
+ const meta = [img.mediaType, formatBytes(img.originalSize)].filter(Boolean).join(', ');
187
+ const caption = meta ? `${label} (${meta})` : label;
188
+ if (img.url) {
189
+ return `**${escapeMarkdown(caption)}**\n\n![${escapeAltText(label)}](${img.url})`;
190
+ }
191
+ return `**${escapeMarkdown(caption)}** — _image upload unavailable; not shown inline_`;
192
+ });
193
+ return `### 🖼️ Images\n\n${blocks.join('\n\n')}`;
194
+ };
195
+
99
196
  export const formatDuration = ms => {
100
197
  if (!ms || ms < 0) return 'unknown';
101
198
 
@@ -405,6 +405,15 @@ export const SOLVE_OPTION_DEFINITIONS = {
405
405
  description: '[EXPERIMENTAL] Post tool output as PR comments in real-time. Supported for --tool claude and --tool codex.',
406
406
  default: false,
407
407
  },
408
+ // Issue #1843: render images that Claude/Codex read/write inline in the PR
409
+ // comments interactive mode posts. Images are committed to hidden custom Git
410
+ // refs and embedded via commit-SHA ?raw=true blob URLs (GitHub strips data: URIs).
411
+ // Disable with --no-interactive-image-upload to fall back to a metadata note.
412
+ 'interactive-image-upload': {
413
+ type: 'boolean',
414
+ description: '[EXPERIMENTAL] When --interactive-mode is on, upload images read/written by the AI to hidden custom Git refs (refs/hive-mind-media/...) and embed them inline in PR comments. Enabled by default; use --no-interactive-image-upload to disable.',
415
+ default: true,
416
+ },
408
417
  // Issue #817: Bidirectional interactive options
409
418
  'accept-incomming-comments-as-input': {
410
419
  type: 'boolean',