@iloom/cli 0.7.5 → 0.7.6

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/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  SessionSummaryService
4
- } from "./chunk-KSXA2NOJ.js";
4
+ } from "./chunk-WT4UGBE2.js";
5
5
  import "./chunk-NXMDEL3F.js";
6
6
  import {
7
7
  CLIIsolationManager,
@@ -9,7 +9,7 @@ import {
9
9
  EnvironmentManager,
10
10
  LoomManager,
11
11
  ResourceCleanup
12
- } from "./chunk-77VLG2KP.js";
12
+ } from "./chunk-ETY2SBW5.js";
13
13
  import {
14
14
  BuildRunner,
15
15
  MergeManager
@@ -60,13 +60,13 @@ import {
60
60
  import "./chunk-XPKN3QWY.js";
61
61
  import {
62
62
  PRManager
63
- } from "./chunk-TB6475EW.js";
63
+ } from "./chunk-6YAMWLCP.js";
64
64
  import {
65
65
  openBrowser
66
66
  } from "./chunk-YETJNRQM.js";
67
67
  import {
68
68
  IssueManagementProviderFactory
69
- } from "./chunk-LZBSLO6S.js";
69
+ } from "./chunk-NPEMVE27.js";
70
70
  import "./chunk-HBJITKSZ.js";
71
71
  import {
72
72
  getConfiguredRepoFromSettings,
@@ -2522,7 +2522,7 @@ program.command("finish").alias("dn").description("Merge work and cleanup worksp
2522
2522
  program.command("commit").alias("c").description("Commit all uncommitted files with issue reference").option("-m, --message <text>", "Custom commit message (skip Claude generation)").option("--fixes", 'Use "Fixes #N" trailer instead of "Refs #N" (closes issue)').option("--no-review", "Skip commit message review prompt").option("--json", "Output result as JSON (implies --no-review)").option("--wip-commit", "Quick WIP commit: skip validations and pre-commit hooks").action(async (options) => {
2523
2523
  const executeAction = async () => {
2524
2524
  try {
2525
- const { CommitCommand } = await import("./commit-NGMDWWAP.js");
2525
+ const { CommitCommand } = await import("./commit-3ULFKXNB.js");
2526
2526
  const command = new CommitCommand();
2527
2527
  const noReview = options.review === false || options.json === true;
2528
2528
  const result = await command.execute({
@@ -2664,7 +2664,7 @@ program.command("compile").alias("typecheck").description("Run the compile or ty
2664
2664
  program.command("cleanup").alias("remove").alias("clean").description("Remove workspaces").argument("[identifier]", "Branch name or issue number to cleanup (auto-detected)").option("-l, --list", "List all worktrees").option("-a, --all", "Remove all worktrees (interactive confirmation)").option("-i, --issue <number>", "Cleanup by issue number", parseInt).option("-f, --force", "Skip confirmations and force removal").option("--dry-run", "Show what would be done without doing it").option("--json", "Output result as JSON").option("--defer <ms>", "Wait specified milliseconds before cleanup", parseInt).action(async (identifier, options) => {
2665
2665
  const executeAction = async () => {
2666
2666
  try {
2667
- const { CleanupCommand } = await import("./cleanup-DB7EFBF3.js");
2667
+ const { CleanupCommand } = await import("./cleanup-IO4KV2DL.js");
2668
2668
  const command = new CleanupCommand();
2669
2669
  const input = {
2670
2670
  options: options ?? {}
@@ -3180,7 +3180,7 @@ program.command("test-prefix").description("Test worktree prefix configuration -
3180
3180
  program.command("summary").description("Generate Claude session summary for a loom").argument("[identifier]", "Issue number, PR number (pr/123), or branch name (auto-detected if omitted)").option("--with-comment", "Post summary as a comment to the issue/PR").option("--json", "Output result as JSON").action(async (identifier, options) => {
3181
3181
  const executeAction = async () => {
3182
3182
  try {
3183
- const { SummaryCommand } = await import("./summary-2KLNHVTN.js");
3183
+ const { SummaryCommand } = await import("./summary-MPOOQIOX.js");
3184
3184
  const command = new SummaryCommand();
3185
3185
  const result = await command.execute({ identifier, options });
3186
3186
  if (options.json && result) {
@@ -10,7 +10,7 @@ import {
10
10
  import "./chunk-XPKN3QWY.js";
11
11
  import {
12
12
  IssueManagementProviderFactory
13
- } from "./chunk-LZBSLO6S.js";
13
+ } from "./chunk-NPEMVE27.js";
14
14
  import "./chunk-HBJITKSZ.js";
15
15
  import {
16
16
  extractIssueNumber,
@@ -234,4 +234,4 @@ export {
234
234
  CommitCommand,
235
235
  WorktreeValidationError
236
236
  };
237
- //# sourceMappingURL=commit-NGMDWWAP.js.map
237
+ //# sourceMappingURL=commit-3ULFKXNB.js.map
@@ -236,6 +236,244 @@ async function addSubIssue(parentNodeId, childNodeId) {
236
236
  ]);
237
237
  }
238
238
 
239
+ // src/utils/image-processor.ts
240
+ import { tmpdir } from "os";
241
+ import { join, extname } from "path";
242
+ import { existsSync as existsSync2, mkdirSync, createWriteStream, unlinkSync } from "fs";
243
+ import { pipeline } from "stream/promises";
244
+ import { Readable } from "stream";
245
+ import { createHash } from "crypto";
246
+ import { execa as execa3 } from "execa";
247
+ var SUPPORTED_EXTENSIONS = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"];
248
+ var MAX_IMAGE_SIZE = 10 * 1024 * 1024;
249
+ var REQUEST_TIMEOUT_MS = 3e4;
250
+ var CACHE_DIR = join(tmpdir(), "iloom-images");
251
+ var cachedGitHubToken;
252
+ function extractMarkdownImageUrls(content) {
253
+ if (!content) {
254
+ return [];
255
+ }
256
+ const matches = [];
257
+ const markdownRegex = /!\[([^\]]*)\]\(((?:[^()\s]|\((?:[^()\s]|\([^()]*\))*\))+)\)/g;
258
+ let match;
259
+ while ((match = markdownRegex.exec(content)) !== null) {
260
+ const url = match[2];
261
+ if (url) {
262
+ matches.push({
263
+ fullMatch: match[0],
264
+ url,
265
+ isMarkdown: true
266
+ });
267
+ }
268
+ }
269
+ const htmlImgRegex = /<img\s+[^>]*src=["']([^"']+)["'][^>]*\/?>/gi;
270
+ while ((match = htmlImgRegex.exec(content)) !== null) {
271
+ const url = match[1];
272
+ if (url) {
273
+ matches.push({
274
+ fullMatch: match[0],
275
+ url,
276
+ isMarkdown: false
277
+ });
278
+ }
279
+ }
280
+ return matches;
281
+ }
282
+ function isAuthenticatedImageUrl(url) {
283
+ try {
284
+ const parsedUrl = new URL(url);
285
+ const hostname = parsedUrl.hostname.toLowerCase();
286
+ if (hostname === "uploads.linear.app") {
287
+ return true;
288
+ }
289
+ if (hostname === "private-user-images.githubusercontent.com") {
290
+ return true;
291
+ }
292
+ if (hostname === "github.com" && parsedUrl.pathname.startsWith("/user-attachments/assets/")) {
293
+ return true;
294
+ }
295
+ return false;
296
+ } catch {
297
+ return false;
298
+ }
299
+ }
300
+ function getExtensionFromUrl(url) {
301
+ try {
302
+ const parsedUrl = new URL(url);
303
+ const pathname = parsedUrl.pathname;
304
+ const ext = extname(pathname).toLowerCase();
305
+ if (SUPPORTED_EXTENSIONS.includes(ext)) {
306
+ return ext;
307
+ }
308
+ return null;
309
+ } catch {
310
+ return null;
311
+ }
312
+ }
313
+ function getCacheKey(url) {
314
+ const parsedUrl = new URL(url);
315
+ if (parsedUrl.hostname === "private-user-images.githubusercontent.com") {
316
+ parsedUrl.searchParams.delete("jwt");
317
+ }
318
+ const stableUrl = parsedUrl.toString();
319
+ const hash = createHash("sha256").update(stableUrl).digest("hex").slice(0, 16);
320
+ const ext = getExtensionFromUrl(url) ?? ".png";
321
+ return `${hash}${ext}`;
322
+ }
323
+ function getCachedImagePath(url) {
324
+ const cacheKey = getCacheKey(url);
325
+ const cachedPath = join(CACHE_DIR, cacheKey);
326
+ if (existsSync2(cachedPath)) {
327
+ return cachedPath;
328
+ }
329
+ return void 0;
330
+ }
331
+ async function getAuthToken(provider) {
332
+ if (provider === "github") {
333
+ if (cachedGitHubToken !== void 0) {
334
+ return cachedGitHubToken;
335
+ }
336
+ try {
337
+ const result = await execa3("gh", ["auth", "token"]);
338
+ cachedGitHubToken = result.stdout.trim();
339
+ return cachedGitHubToken;
340
+ } catch (error) {
341
+ const message = error instanceof Error ? error.message : String(error);
342
+ logger.warn(`Failed to get GitHub auth token via gh CLI: ${message}`);
343
+ return void 0;
344
+ }
345
+ }
346
+ if (provider === "linear") {
347
+ return process.env.LINEAR_API_TOKEN;
348
+ }
349
+ return void 0;
350
+ }
351
+ async function downloadAndSaveImage(url, destPath, authHeader) {
352
+ const headers = {};
353
+ if (authHeader) {
354
+ headers["Authorization"] = authHeader;
355
+ }
356
+ const controller = new AbortController();
357
+ const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
358
+ try {
359
+ const response = await fetch(url, { headers, signal: controller.signal });
360
+ if (!response.ok) {
361
+ throw new Error(`Failed to download image: ${response.status} ${response.statusText}`);
362
+ }
363
+ const contentLength = response.headers.get("Content-Length");
364
+ if (contentLength && parseInt(contentLength, 10) > MAX_IMAGE_SIZE) {
365
+ throw new Error(`Image too large: ${contentLength} bytes exceeds ${MAX_IMAGE_SIZE} byte limit`);
366
+ }
367
+ if (!response.body) {
368
+ throw new Error("Response body is null");
369
+ }
370
+ const reader = response.body.getReader();
371
+ let bytesWritten = 0;
372
+ const nodeReadable = new Readable({
373
+ async read() {
374
+ try {
375
+ const { done, value } = await reader.read();
376
+ if (done) {
377
+ this.push(null);
378
+ return;
379
+ }
380
+ bytesWritten += value.byteLength;
381
+ if (bytesWritten > MAX_IMAGE_SIZE) {
382
+ reader.cancel();
383
+ this.destroy(new Error(`Image too large: ${bytesWritten} bytes exceeds ${MAX_IMAGE_SIZE} byte limit`));
384
+ return;
385
+ }
386
+ this.push(Buffer.from(value));
387
+ } catch (err) {
388
+ this.destroy(err instanceof Error ? err : new Error(String(err)));
389
+ }
390
+ }
391
+ });
392
+ if (!existsSync2(CACHE_DIR)) {
393
+ mkdirSync(CACHE_DIR, { recursive: true });
394
+ }
395
+ const writeStream = createWriteStream(destPath);
396
+ try {
397
+ await pipeline(nodeReadable, writeStream);
398
+ } catch (pipelineError) {
399
+ try {
400
+ if (existsSync2(destPath)) {
401
+ unlinkSync(destPath);
402
+ }
403
+ } catch {
404
+ }
405
+ throw pipelineError;
406
+ }
407
+ } catch (error) {
408
+ if (error instanceof Error && error.name === "AbortError") {
409
+ throw new Error(`Image download timed out after ${REQUEST_TIMEOUT_MS}ms`);
410
+ }
411
+ throw error;
412
+ } finally {
413
+ clearTimeout(timeoutId);
414
+ }
415
+ }
416
+ function getCacheDestPath(url) {
417
+ if (!existsSync2(CACHE_DIR)) {
418
+ mkdirSync(CACHE_DIR, { recursive: true });
419
+ }
420
+ const cacheKey = getCacheKey(url);
421
+ return join(CACHE_DIR, cacheKey);
422
+ }
423
+ function rewriteMarkdownUrls(content, urlMap) {
424
+ let result = content;
425
+ for (const [originalUrl, localPath] of urlMap) {
426
+ const escapedUrl = originalUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
427
+ const urlRegex = new RegExp(escapedUrl, "g");
428
+ result = result.replace(urlRegex, localPath);
429
+ }
430
+ return result;
431
+ }
432
+ async function processMarkdownImages(content, provider) {
433
+ if (!content) {
434
+ return "";
435
+ }
436
+ const images = extractMarkdownImageUrls(content);
437
+ if (images.length === 0) {
438
+ return content;
439
+ }
440
+ const authImages = images.filter((img) => isAuthenticatedImageUrl(img.url));
441
+ if (authImages.length === 0) {
442
+ return content;
443
+ }
444
+ const authToken = await getAuthToken(provider);
445
+ const uniqueUrls = [...new Set(authImages.map((img) => img.url))];
446
+ const urlMap = /* @__PURE__ */ new Map();
447
+ const downloadPromises = uniqueUrls.map(async (url) => {
448
+ try {
449
+ const cachedPath = getCachedImagePath(url);
450
+ if (cachedPath) {
451
+ logger.debug(`Using cached image: ${cachedPath}`);
452
+ return { url, localPath: cachedPath };
453
+ }
454
+ logger.debug(`Downloading image: ${url}`);
455
+ const destPath = getCacheDestPath(url);
456
+ await downloadAndSaveImage(
457
+ url,
458
+ destPath,
459
+ authToken ? `Bearer ${authToken}` : void 0
460
+ );
461
+ return { url, localPath: destPath };
462
+ } catch (error) {
463
+ const message = error instanceof Error ? error.message : String(error);
464
+ logger.warn(`Failed to download image ${url}: ${message}`);
465
+ return null;
466
+ }
467
+ });
468
+ const results = await Promise.all(downloadPromises);
469
+ for (const result of results) {
470
+ if (result !== null) {
471
+ urlMap.set(result.url, result.localPath);
472
+ }
473
+ }
474
+ return rewriteMarkdownUrls(content, urlMap);
475
+ }
476
+
239
477
  // src/mcp/GitHubIssueManagementProvider.ts
240
478
  function normalizeAuthor(author) {
241
479
  if (!author) return null;
@@ -314,6 +552,12 @@ var GitHubIssueManagementProvider = class {
314
552
  ...comment.updatedAt && { updatedAt: comment.updatedAt }
315
553
  }));
316
554
  }
555
+ result.body = await processMarkdownImages(result.body, "github");
556
+ if (result.comments) {
557
+ for (const comment of result.comments) {
558
+ comment.body = await processMarkdownImages(comment.body, "github");
559
+ }
560
+ }
317
561
  return result;
318
562
  }
319
563
  /**
@@ -382,6 +626,12 @@ var GitHubIssueManagementProvider = class {
382
626
  ...comment.updatedAt && { updatedAt: comment.updatedAt }
383
627
  }));
384
628
  }
629
+ result.body = await processMarkdownImages(result.body, "github");
630
+ if (result.comments) {
631
+ for (const comment of result.comments) {
632
+ comment.body = await processMarkdownImages(comment.body, "github");
633
+ }
634
+ }
385
635
  return result;
386
636
  }
387
637
  /**
@@ -401,9 +651,10 @@ var GitHubIssueManagementProvider = class {
401
651
  "--jq",
402
652
  "{id: .id, body: .body, user: .user, created_at: .created_at, updated_at: .updated_at, html_url: .html_url, reactions: .reactions}"
403
653
  ]);
654
+ const processedBody = await processMarkdownImages(raw.body, "github");
404
655
  return {
405
656
  id: String(raw.id),
406
- body: raw.body,
657
+ body: processedBody,
407
658
  author: normalizeAuthor(raw.user),
408
659
  created_at: raw.created_at,
409
660
  ...raw.updated_at && { updated_at: raw.updated_at },
@@ -752,7 +1003,7 @@ async function fetchLinearIssueComments(identifier) {
752
1003
 
753
1004
  // src/utils/linear-markup-converter.ts
754
1005
  import { appendFileSync } from "fs";
755
- import { join, dirname, basename, extname } from "path";
1006
+ import { join as join2, dirname, basename, extname as extname2 } from "path";
756
1007
  var LinearMarkupConverter = class {
757
1008
  /**
758
1009
  * Convert HTML details/summary blocks to Linear's collapsible format
@@ -888,7 +1139,7 @@ ${content}
888
1139
  */
889
1140
  static getTimestampedLogPath(logFilePath) {
890
1141
  const dir = dirname(logFilePath);
891
- const ext = extname(logFilePath);
1142
+ const ext = extname2(logFilePath);
892
1143
  const base = basename(logFilePath, ext);
893
1144
  const now = /* @__PURE__ */ new Date();
894
1145
  const timestamp = [
@@ -900,7 +1151,7 @@ ${content}
900
1151
  String(now.getMinutes()).padStart(2, "0"),
901
1152
  String(now.getSeconds()).padStart(2, "0")
902
1153
  ].join("");
903
- return join(dir, `${base}-${timestamp}${ext}`);
1154
+ return join2(dir, `${base}-${timestamp}${ext}`);
904
1155
  }
905
1156
  };
906
1157
 
@@ -949,6 +1200,12 @@ var LinearIssueManagementProvider = class {
949
1200
  } catch {
950
1201
  }
951
1202
  }
1203
+ result.body = await processMarkdownImages(result.body, "linear");
1204
+ if (result.comments) {
1205
+ for (const comment of result.comments) {
1206
+ comment.body = await processMarkdownImages(comment.body, "linear");
1207
+ }
1208
+ }
952
1209
  return result;
953
1210
  }
954
1211
  /**
@@ -982,9 +1239,10 @@ var LinearIssueManagementProvider = class {
982
1239
  async getComment(input) {
983
1240
  const { commentId } = input;
984
1241
  const raw = await getLinearComment(commentId);
1242
+ const processedBody = await processMarkdownImages(raw.body, "linear");
985
1243
  return {
986
1244
  id: raw.id,
987
- body: raw.body,
1245
+ body: processedBody,
988
1246
  author: null,
989
1247
  // Linear SDK doesn't return comment author info in basic fetch
990
1248
  created_at: raw.createdAt