imgx-mcp 1.3.0 → 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,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.0 (2026-03-04)
4
+
5
+ ### Added
6
+
7
+ - **Sequential file naming for edit chains** — `edit_last` now generates sequential filenames based on the origin file: `cover.png` → `cover-1.png` → `cover-2.png`. Default UUID-based names follow the same pattern: `imgx-a1b2c3d4.png` → `imgx-a1b2c3d4-1.png`
8
+ - **File deletion on undo + re-edit** — When editing after undo, spliced (abandoned) entries' files are deleted from disk, preventing orphaned images
9
+ - **Session base info tracking** — `Session` now stores `baseName`, `baseExt`, `baseDir` to maintain file identity across the edit chain
10
+ - **`getSessionChainNumber()` API** — returns the next sequential number for chained edits
11
+ - **`getSessionBaseInfo()` / `setSessionBaseInfo()` API** — read/write the origin file identity on the active session
12
+ - 15 new tests (total: 81 tests)
13
+
14
+ ### Changed
15
+
16
+ - **`saveImage()` signature** — new optional `isChained` parameter for sequential naming (backward compatible, defaults to falsy)
17
+ - Legacy sessions without `baseName` fall back to UUID naming when `isChained` is true
18
+
3
19
  ## 1.3.0 (2026-03-04)
4
20
 
5
21
  ### Added
package/README.md CHANGED
@@ -129,7 +129,18 @@ generate → edit_last → edit_last → edit_last
129
129
  ↑ current
130
130
  ```
131
131
 
132
- After undo, calling `edit_last` branches from the current position (newer entries are discarded).
132
+ After undo, calling `edit_last` branches from the current position (abandoned entries and their files are deleted from disk).
133
+
134
+ **File naming** — `edit_last` generates sequential filenames based on the origin file:
135
+
136
+ ```
137
+ generate_image → cover.png
138
+ edit_last → cover-1.png
139
+ edit_last → cover-2.png
140
+
141
+ generate_image (no output) → imgx-a1b2c3d4.png
142
+ edit_last → imgx-a1b2c3d4-1.png
143
+ ```
133
144
 
134
145
  **Session switching** — Use `edit_history` to see all sessions, then `switch_session` to resume a previous session. The `edit_last` tool will use the current position in the switched session.
135
146
 
@@ -39226,10 +39226,239 @@ function getApiKeyFromEnv() {
39226
39226
  }
39227
39227
 
39228
39228
  // build/core/storage.js
39229
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
39230
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
39229
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
39230
+ import { dirname as dirname3, join as join3, resolve as resolve3 } from "node:path";
39231
+ import { randomUUID as randomUUID2 } from "node:crypto";
39232
+ import { homedir as homedir3 } from "node:os";
39233
+
39234
+ // build/core/history.js
39235
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, rmSync } from "node:fs";
39236
+ import { basename as basename3, dirname as dirname2, extname, join as join2, resolve as resolve2, sep } from "node:path";
39237
+ import { homedir as homedir2, platform as platform2 } from "node:os";
39231
39238
  import { randomUUID } from "node:crypto";
39232
- import { homedir as homedir2 } from "node:os";
39239
+ var HISTORY_FILE = "output-history.json";
39240
+ function globalHistoryDir() {
39241
+ if (platform2() === "win32") {
39242
+ return join2(process.env.APPDATA || join2(homedir2(), "AppData", "Roaming"), "imgx");
39243
+ }
39244
+ return join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "imgx");
39245
+ }
39246
+ function historyDir() {
39247
+ if (process.env.IMGX_TEST_CONFIG_DIR)
39248
+ return process.env.IMGX_TEST_CONFIG_DIR;
39249
+ const projectRoot = findProjectRoot();
39250
+ if (projectRoot)
39251
+ return join2(projectRoot, ".imgx");
39252
+ return globalHistoryDir();
39253
+ }
39254
+ function historyPath() {
39255
+ return join2(historyDir(), HISTORY_FILE);
39256
+ }
39257
+ function emptyHistory() {
39258
+ return {
39259
+ sessions: [],
39260
+ activeSessionId: null,
39261
+ maxEntriesPerSession: 10
39262
+ };
39263
+ }
39264
+ function loadHistory() {
39265
+ try {
39266
+ const raw = readFileSync2(historyPath(), "utf-8");
39267
+ return JSON.parse(raw);
39268
+ } catch {
39269
+ return emptyHistory();
39270
+ }
39271
+ }
39272
+ function saveHistory(history) {
39273
+ const dir = historyDir();
39274
+ mkdirSync2(dir, { recursive: true });
39275
+ writeFileSync2(historyPath(), JSON.stringify(history, null, 2) + "\n", "utf-8");
39276
+ }
39277
+ function createSession() {
39278
+ return {
39279
+ id: `s-${randomUUID().slice(0, 8)}`,
39280
+ entries: [],
39281
+ cursor: 0
39282
+ };
39283
+ }
39284
+ function pushHistory(entry, opts) {
39285
+ const history = loadHistory();
39286
+ if (opts.newSession) {
39287
+ const session2 = createSession();
39288
+ if (opts.sessionId)
39289
+ session2.id = opts.sessionId;
39290
+ if (opts.outputDir)
39291
+ session2.outputDir = opts.outputDir;
39292
+ if (entry.filePaths.length > 0) {
39293
+ const fp = entry.filePaths[0];
39294
+ const ext = extname(fp);
39295
+ session2.baseName = basename3(fp, ext);
39296
+ session2.baseExt = ext;
39297
+ session2.baseDir = dirname2(fp);
39298
+ }
39299
+ session2.entries.push(entry);
39300
+ history.sessions.push(session2);
39301
+ history.activeSessionId = session2.id;
39302
+ saveHistory(history);
39303
+ return session2.id;
39304
+ }
39305
+ const session = history.sessions.find((s2) => s2.id === history.activeSessionId);
39306
+ if (!session)
39307
+ throw new Error("No active session. Run generate first.");
39308
+ if (session.cursor > 0) {
39309
+ const removed = session.entries.splice(0, session.cursor);
39310
+ for (const entry2 of removed) {
39311
+ for (const fp of entry2.filePaths) {
39312
+ try {
39313
+ if (existsSync2(fp))
39314
+ rmSync(fp);
39315
+ } catch {
39316
+ }
39317
+ }
39318
+ }
39319
+ session.cursor = 0;
39320
+ }
39321
+ session.entries.unshift(entry);
39322
+ while (session.entries.length > history.maxEntriesPerSession) {
39323
+ session.entries.splice(session.entries.length - 2, 1);
39324
+ }
39325
+ saveHistory(history);
39326
+ return session.id;
39327
+ }
39328
+ function getActiveEntry() {
39329
+ const history = loadHistory();
39330
+ if (!history.activeSessionId)
39331
+ return void 0;
39332
+ const session = history.sessions.find((s2) => s2.id === history.activeSessionId);
39333
+ if (!session || session.entries.length === 0)
39334
+ return void 0;
39335
+ return session.entries[session.cursor];
39336
+ }
39337
+ function getActiveSession() {
39338
+ const history = loadHistory();
39339
+ if (!history.activeSessionId) {
39340
+ throw new Error("No active session. Run generate first.");
39341
+ }
39342
+ const session = history.sessions.find((s2) => s2.id === history.activeSessionId);
39343
+ if (!session) {
39344
+ throw new Error("No active session. Run generate first.");
39345
+ }
39346
+ return { history, session };
39347
+ }
39348
+ function undoHistory() {
39349
+ const { history, session } = getActiveSession();
39350
+ const maxCursor = session.entries.length - 1;
39351
+ if (session.cursor >= maxCursor) {
39352
+ throw new Error("Already at the oldest entry in this session");
39353
+ }
39354
+ session.cursor += 1;
39355
+ saveHistory(history);
39356
+ return {
39357
+ entry: session.entries[session.cursor],
39358
+ position: `${session.cursor + 1}/${session.entries.length}`
39359
+ };
39360
+ }
39361
+ function redoHistory() {
39362
+ const { history, session } = getActiveSession();
39363
+ if (session.cursor <= 0) {
39364
+ throw new Error("Already at the latest entry");
39365
+ }
39366
+ session.cursor -= 1;
39367
+ saveHistory(history);
39368
+ return {
39369
+ entry: session.entries[session.cursor],
39370
+ position: `${session.cursor + 1}/${session.entries.length}`
39371
+ };
39372
+ }
39373
+ function switchSession(sessionId) {
39374
+ const history = loadHistory();
39375
+ const session = history.sessions.find((s2) => s2.id === sessionId);
39376
+ if (!session) {
39377
+ throw new Error(`Session not found: ${sessionId}`);
39378
+ }
39379
+ history.activeSessionId = sessionId;
39380
+ saveHistory(history);
39381
+ }
39382
+ function getHistory() {
39383
+ return loadHistory();
39384
+ }
39385
+ function clearHistory() {
39386
+ const history = loadHistory();
39387
+ const filePaths = [];
39388
+ for (const session of history.sessions) {
39389
+ for (const entry of session.entries) {
39390
+ filePaths.push(...entry.filePaths);
39391
+ }
39392
+ }
39393
+ history.sessions = [];
39394
+ history.activeSessionId = null;
39395
+ saveHistory(history);
39396
+ return { filePaths };
39397
+ }
39398
+ function clearSession(sessionId) {
39399
+ const history = loadHistory();
39400
+ const idx = history.sessions.findIndex((s2) => s2.id === sessionId);
39401
+ if (idx === -1)
39402
+ throw new Error(`Session not found: ${sessionId}`);
39403
+ const [removed] = history.sessions.splice(idx, 1);
39404
+ const filePaths = [];
39405
+ for (const entry of removed.entries)
39406
+ filePaths.push(...entry.filePaths);
39407
+ if (history.activeSessionId === sessionId)
39408
+ history.activeSessionId = null;
39409
+ saveHistory(history);
39410
+ return { filePaths };
39411
+ }
39412
+ function clearGlobalHistory() {
39413
+ const globalPath = join2(globalHistoryDir(), HISTORY_FILE);
39414
+ try {
39415
+ const raw = readFileSync2(globalPath, "utf-8");
39416
+ const history = JSON.parse(raw);
39417
+ const filePaths = [];
39418
+ for (const session of history.sessions) {
39419
+ for (const entry of session.entries)
39420
+ filePaths.push(...entry.filePaths);
39421
+ }
39422
+ const dir = globalHistoryDir();
39423
+ mkdirSync2(dir, { recursive: true });
39424
+ writeFileSync2(globalPath, JSON.stringify(emptyHistory(), null, 2) + "\n", "utf-8");
39425
+ return { filePaths };
39426
+ } catch {
39427
+ return { filePaths: [] };
39428
+ }
39429
+ }
39430
+ function updateHistoryPaths(oldBase, newBase) {
39431
+ const history = loadHistory();
39432
+ for (const session of history.sessions) {
39433
+ for (const entry of session.entries) {
39434
+ entry.filePaths = entry.filePaths.map((p) => p.startsWith(oldBase) ? newBase + p.slice(oldBase.length) : p);
39435
+ if (entry.inputImage && entry.inputImage.startsWith(oldBase)) {
39436
+ entry.inputImage = newBase + entry.inputImage.slice(oldBase.length);
39437
+ }
39438
+ }
39439
+ }
39440
+ saveHistory(history);
39441
+ }
39442
+ function getSessionChainNumber() {
39443
+ const history = loadHistory();
39444
+ if (!history.activeSessionId)
39445
+ return null;
39446
+ const session = history.sessions.find((s2) => s2.id === history.activeSessionId);
39447
+ if (!session || session.entries.length === 0)
39448
+ return null;
39449
+ return session.entries.length;
39450
+ }
39451
+ function getSessionBaseInfo() {
39452
+ const history = loadHistory();
39453
+ if (!history.activeSessionId)
39454
+ return null;
39455
+ const session = history.sessions.find((s2) => s2.id === history.activeSessionId);
39456
+ if (!session?.baseName)
39457
+ return null;
39458
+ return { baseName: session.baseName, baseExt: session.baseExt, baseDir: session.baseDir };
39459
+ }
39460
+
39461
+ // build/core/storage.js
39233
39462
  var MIME_TO_EXT = {
39234
39463
  "image/png": ".png",
39235
39464
  "image/jpeg": ".jpg",
@@ -39237,7 +39466,7 @@ var MIME_TO_EXT = {
39237
39466
  };
39238
39467
  function readImageAsBase64(filePath) {
39239
39468
  const absPath = resolveProjectPath(filePath);
39240
- const buffer = readFileSync2(absPath);
39469
+ const buffer = readFileSync3(absPath);
39241
39470
  const ext = filePath.split(".").pop()?.toLowerCase();
39242
39471
  const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "webp" ? "image/webp" : "image/png";
39243
39472
  return { data: buffer.toString("base64"), mimeType };
@@ -39250,21 +39479,29 @@ function fallbackOutputDir(outputDir) {
39250
39479
  return resolveProjectPath(configured);
39251
39480
  const projectRoot = findProjectRoot();
39252
39481
  if (projectRoot)
39253
- return join2(projectRoot, ".imgx");
39254
- return join2(homedir2(), "Pictures", "imgx");
39482
+ return join3(projectRoot, ".imgx");
39483
+ return join3(homedir3(), "Pictures", "imgx");
39255
39484
  }
39256
- function saveImage(image, outputPath, outputDir, sessionId) {
39485
+ function saveImage(image, outputPath, outputDir, sessionId, isChained) {
39257
39486
  const ext = MIME_TO_EXT[image.mimeType] || ".png";
39258
39487
  let filePath;
39259
- if (outputPath) {
39488
+ if (isChained) {
39489
+ const baseInfo = getSessionBaseInfo();
39490
+ const chainNum = getSessionChainNumber();
39491
+ if (baseInfo && chainNum !== null) {
39492
+ filePath = resolve3(baseInfo.baseDir, `${baseInfo.baseName}-${chainNum}${baseInfo.baseExt}`);
39493
+ } else {
39494
+ filePath = resolve3(fallbackOutputDir(outputDir), sessionId || "", `imgx-${randomUUID2().slice(0, 8)}${ext}`);
39495
+ }
39496
+ } else if (outputPath) {
39260
39497
  filePath = resolveProjectPath(outputPath);
39261
39498
  } else {
39262
39499
  const baseDir = fallbackOutputDir(outputDir);
39263
- const targetDir = sessionId ? join2(baseDir, sessionId) : baseDir;
39264
- filePath = resolve2(targetDir, `imgx-${randomUUID().slice(0, 8)}${ext}`);
39500
+ const targetDir = sessionId ? join3(baseDir, sessionId) : baseDir;
39501
+ filePath = resolve3(targetDir, `imgx-${randomUUID2().slice(0, 8)}${ext}`);
39265
39502
  }
39266
- mkdirSync2(dirname2(filePath), { recursive: true });
39267
- writeFileSync2(filePath, image.data);
39503
+ mkdirSync3(dirname3(filePath), { recursive: true });
39504
+ writeFileSync3(filePath, image.data);
39268
39505
  return filePath;
39269
39506
  }
39270
39507
 
@@ -39399,7 +39636,7 @@ function initGemini() {
39399
39636
  }
39400
39637
 
39401
39638
  // build/providers/openai/client.js
39402
- import { readFileSync as readFileSync3 } from "node:fs";
39639
+ import { readFileSync as readFileSync4 } from "node:fs";
39403
39640
 
39404
39641
  // build/providers/openai/capabilities.js
39405
39642
  var OPENAI_PROVIDER_INFO = {
@@ -39513,7 +39750,7 @@ var OpenAIProvider = class {
39513
39750
  async edit(input, model) {
39514
39751
  const modelName = model || this.info.defaultModel;
39515
39752
  const absPath = resolveProjectPath(input.inputImage);
39516
- const imageBuffer = readFileSync3(absPath);
39753
+ const imageBuffer = readFileSync4(absPath);
39517
39754
  const ext = absPath.split(".").pop()?.toLowerCase();
39518
39755
  const contentType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "webp" ? "image/webp" : "image/png";
39519
39756
  const fields = {
@@ -39584,201 +39821,8 @@ function initOpenAI() {
39584
39821
  registerProvider(new OpenAIProvider(apiKey));
39585
39822
  }
39586
39823
 
39587
- // build/core/history.js
39588
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
39589
- import { join as join3, resolve as resolve3, sep } from "node:path";
39590
- import { homedir as homedir3, platform as platform2 } from "node:os";
39591
- import { randomUUID as randomUUID2 } from "node:crypto";
39592
- var HISTORY_FILE = "output-history.json";
39593
- function globalHistoryDir() {
39594
- if (platform2() === "win32") {
39595
- return join3(process.env.APPDATA || join3(homedir3(), "AppData", "Roaming"), "imgx");
39596
- }
39597
- return join3(process.env.XDG_CONFIG_HOME || join3(homedir3(), ".config"), "imgx");
39598
- }
39599
- function historyDir() {
39600
- if (process.env.IMGX_TEST_CONFIG_DIR)
39601
- return process.env.IMGX_TEST_CONFIG_DIR;
39602
- const projectRoot = findProjectRoot();
39603
- if (projectRoot)
39604
- return join3(projectRoot, ".imgx");
39605
- return globalHistoryDir();
39606
- }
39607
- function historyPath() {
39608
- return join3(historyDir(), HISTORY_FILE);
39609
- }
39610
- function emptyHistory() {
39611
- return {
39612
- sessions: [],
39613
- activeSessionId: null,
39614
- maxEntriesPerSession: 10
39615
- };
39616
- }
39617
- function loadHistory() {
39618
- try {
39619
- const raw = readFileSync4(historyPath(), "utf-8");
39620
- return JSON.parse(raw);
39621
- } catch {
39622
- return emptyHistory();
39623
- }
39624
- }
39625
- function saveHistory(history) {
39626
- const dir = historyDir();
39627
- mkdirSync3(dir, { recursive: true });
39628
- writeFileSync3(historyPath(), JSON.stringify(history, null, 2) + "\n", "utf-8");
39629
- }
39630
- function createSession() {
39631
- return {
39632
- id: `s-${randomUUID2().slice(0, 8)}`,
39633
- entries: [],
39634
- cursor: 0
39635
- };
39636
- }
39637
- function pushHistory(entry, opts) {
39638
- const history = loadHistory();
39639
- if (opts.newSession) {
39640
- const session2 = createSession();
39641
- if (opts.sessionId)
39642
- session2.id = opts.sessionId;
39643
- if (opts.outputDir)
39644
- session2.outputDir = opts.outputDir;
39645
- session2.entries.push(entry);
39646
- history.sessions.push(session2);
39647
- history.activeSessionId = session2.id;
39648
- saveHistory(history);
39649
- return session2.id;
39650
- }
39651
- const session = history.sessions.find((s2) => s2.id === history.activeSessionId);
39652
- if (!session)
39653
- throw new Error("No active session. Run generate first.");
39654
- if (session.cursor > 0) {
39655
- session.entries.splice(0, session.cursor);
39656
- session.cursor = 0;
39657
- }
39658
- session.entries.unshift(entry);
39659
- while (session.entries.length > history.maxEntriesPerSession) {
39660
- session.entries.splice(session.entries.length - 2, 1);
39661
- }
39662
- saveHistory(history);
39663
- return session.id;
39664
- }
39665
- function getActiveEntry() {
39666
- const history = loadHistory();
39667
- if (!history.activeSessionId)
39668
- return void 0;
39669
- const session = history.sessions.find((s2) => s2.id === history.activeSessionId);
39670
- if (!session || session.entries.length === 0)
39671
- return void 0;
39672
- return session.entries[session.cursor];
39673
- }
39674
- function getActiveSession() {
39675
- const history = loadHistory();
39676
- if (!history.activeSessionId) {
39677
- throw new Error("No active session. Run generate first.");
39678
- }
39679
- const session = history.sessions.find((s2) => s2.id === history.activeSessionId);
39680
- if (!session) {
39681
- throw new Error("No active session. Run generate first.");
39682
- }
39683
- return { history, session };
39684
- }
39685
- function undoHistory() {
39686
- const { history, session } = getActiveSession();
39687
- const maxCursor = session.entries.length - 1;
39688
- if (session.cursor >= maxCursor) {
39689
- throw new Error("Already at the oldest entry in this session");
39690
- }
39691
- session.cursor += 1;
39692
- saveHistory(history);
39693
- return {
39694
- entry: session.entries[session.cursor],
39695
- position: `${session.cursor + 1}/${session.entries.length}`
39696
- };
39697
- }
39698
- function redoHistory() {
39699
- const { history, session } = getActiveSession();
39700
- if (session.cursor <= 0) {
39701
- throw new Error("Already at the latest entry");
39702
- }
39703
- session.cursor -= 1;
39704
- saveHistory(history);
39705
- return {
39706
- entry: session.entries[session.cursor],
39707
- position: `${session.cursor + 1}/${session.entries.length}`
39708
- };
39709
- }
39710
- function switchSession(sessionId) {
39711
- const history = loadHistory();
39712
- const session = history.sessions.find((s2) => s2.id === sessionId);
39713
- if (!session) {
39714
- throw new Error(`Session not found: ${sessionId}`);
39715
- }
39716
- history.activeSessionId = sessionId;
39717
- saveHistory(history);
39718
- }
39719
- function getHistory() {
39720
- return loadHistory();
39721
- }
39722
- function clearHistory() {
39723
- const history = loadHistory();
39724
- const filePaths = [];
39725
- for (const session of history.sessions) {
39726
- for (const entry of session.entries) {
39727
- filePaths.push(...entry.filePaths);
39728
- }
39729
- }
39730
- history.sessions = [];
39731
- history.activeSessionId = null;
39732
- saveHistory(history);
39733
- return { filePaths };
39734
- }
39735
- function clearSession(sessionId) {
39736
- const history = loadHistory();
39737
- const idx = history.sessions.findIndex((s2) => s2.id === sessionId);
39738
- if (idx === -1)
39739
- throw new Error(`Session not found: ${sessionId}`);
39740
- const [removed] = history.sessions.splice(idx, 1);
39741
- const filePaths = [];
39742
- for (const entry of removed.entries)
39743
- filePaths.push(...entry.filePaths);
39744
- if (history.activeSessionId === sessionId)
39745
- history.activeSessionId = null;
39746
- saveHistory(history);
39747
- return { filePaths };
39748
- }
39749
- function clearGlobalHistory() {
39750
- const globalPath = join3(globalHistoryDir(), HISTORY_FILE);
39751
- try {
39752
- const raw = readFileSync4(globalPath, "utf-8");
39753
- const history = JSON.parse(raw);
39754
- const filePaths = [];
39755
- for (const session of history.sessions) {
39756
- for (const entry of session.entries)
39757
- filePaths.push(...entry.filePaths);
39758
- }
39759
- const dir = globalHistoryDir();
39760
- mkdirSync3(dir, { recursive: true });
39761
- writeFileSync3(globalPath, JSON.stringify(emptyHistory(), null, 2) + "\n", "utf-8");
39762
- return { filePaths };
39763
- } catch {
39764
- return { filePaths: [] };
39765
- }
39766
- }
39767
- function updateHistoryPaths(oldBase, newBase) {
39768
- const history = loadHistory();
39769
- for (const session of history.sessions) {
39770
- for (const entry of session.entries) {
39771
- entry.filePaths = entry.filePaths.map((p) => p.startsWith(oldBase) ? newBase + p.slice(oldBase.length) : p);
39772
- if (entry.inputImage && entry.inputImage.startsWith(oldBase)) {
39773
- entry.inputImage = newBase + entry.inputImage.slice(oldBase.length);
39774
- }
39775
- }
39776
- }
39777
- saveHistory(history);
39778
- }
39779
-
39780
39824
  // build/cli/commands/init.js
39781
- import { existsSync as existsSync2, writeFileSync as writeFileSync4 } from "node:fs";
39825
+ import { existsSync as existsSync3, writeFileSync as writeFileSync4 } from "node:fs";
39782
39826
  import { resolve as resolve4 } from "node:path";
39783
39827
 
39784
39828
  // build/cli/output.js
@@ -39801,7 +39845,7 @@ var TEMPLATE = {
39801
39845
  };
39802
39846
  function runInit() {
39803
39847
  const filePath = resolve4(".imgxrc");
39804
- if (existsSync2(filePath)) {
39848
+ if (existsSync3(filePath)) {
39805
39849
  fail(".imgxrc already exists in current directory");
39806
39850
  }
39807
39851
  writeFileSync4(filePath, JSON.stringify(TEMPLATE, null, 2) + "\n", "utf-8");
@@ -39884,7 +39928,7 @@ async function runEdit(provider, args) {
39884
39928
 
39885
39929
  // build/cli/commands/config.js
39886
39930
  import { createInterface } from "node:readline";
39887
- import { existsSync as existsSync3, readdirSync, cpSync, rmSync, mkdirSync as mkdirSync4 } from "node:fs";
39931
+ import { existsSync as existsSync4, readdirSync, cpSync, rmSync as rmSync2, mkdirSync as mkdirSync4 } from "node:fs";
39888
39932
  import { resolve as resolve5 } from "node:path";
39889
39933
  import { homedir as homedir4 } from "node:os";
39890
39934
  function extractProvider(args) {
@@ -39997,7 +40041,7 @@ function setOutputDir(newDir, skipConfirm, noMove) {
39997
40041
  success({ key: "output-dir", status: "saved", outputDir: resolvedNew });
39998
40042
  return;
39999
40043
  }
40000
- if (!existsSync3(oldDir)) {
40044
+ if (!existsSync4(oldDir)) {
40001
40045
  success({ key: "output-dir", status: "saved", outputDir: resolvedNew });
40002
40046
  return;
40003
40047
  }
@@ -40039,7 +40083,7 @@ function setOutputDir(newDir, skipConfirm, noMove) {
40039
40083
  function moveFiles(oldDir, newDir) {
40040
40084
  mkdirSync4(newDir, { recursive: true });
40041
40085
  cpSync(oldDir, newDir, { recursive: true });
40042
- rmSync(oldDir, { recursive: true, force: true });
40086
+ rmSync2(oldDir, { recursive: true, force: true });
40043
40087
  updateHistoryPaths(oldDir, newDir);
40044
40088
  }
40045
40089
  function getKey(key, provider) {
@@ -40088,8 +40132,8 @@ function showAll() {
40088
40132
 
40089
40133
  // build/cli/commands/history.js
40090
40134
  import { createInterface as createInterface2 } from "node:readline";
40091
- import { rmSync as rmSync2, rmdirSync, existsSync as existsSync4 } from "node:fs";
40092
- import { dirname as dirname3 } from "node:path";
40135
+ import { rmSync as rmSync3, rmdirSync, existsSync as existsSync5 } from "node:fs";
40136
+ import { dirname as dirname4 } from "node:path";
40093
40137
  function runHistory(args) {
40094
40138
  const sub = args[0];
40095
40139
  if (!sub) {
@@ -40191,10 +40235,10 @@ function deleteSessionFiles(filePaths) {
40191
40235
  const dirs = /* @__PURE__ */ new Set();
40192
40236
  for (const fp of filePaths) {
40193
40237
  try {
40194
- if (existsSync4(fp)) {
40195
- rmSync2(fp);
40238
+ if (existsSync5(fp)) {
40239
+ rmSync3(fp);
40196
40240
  count++;
40197
- dirs.add(dirname3(fp));
40241
+ dirs.add(dirname4(fp));
40198
40242
  }
40199
40243
  } catch {
40200
40244
  }
@@ -40237,7 +40281,7 @@ function runRedo() {
40237
40281
  }
40238
40282
 
40239
40283
  // build/cli/index.js
40240
- var VERSION2 = "1.3.0";
40284
+ var VERSION2 = "1.4.0";
40241
40285
  var HELP = `imgx v${VERSION2} \u2014 AI image generation and editing for MCP-compatible AI agents
40242
40286
 
40243
40287
  Commands:
@@ -28935,6 +28935,8 @@ __export(history_exports, {
28935
28935
  getActiveEntry: () => getActiveEntry,
28936
28936
  getActiveSessionOutputDir: () => getActiveSessionOutputDir,
28937
28937
  getHistory: () => getHistory,
28938
+ getSessionBaseInfo: () => getSessionBaseInfo,
28939
+ getSessionChainNumber: () => getSessionChainNumber,
28938
28940
  globalHistoryDir: () => globalHistoryDir,
28939
28941
  historyDir: () => historyDir,
28940
28942
  isManagedPath: () => isManagedPath,
@@ -28942,30 +28944,31 @@ __export(history_exports, {
28942
28944
  pushHistory: () => pushHistory,
28943
28945
  redoHistory: () => redoHistory,
28944
28946
  saveHistory: () => saveHistory,
28947
+ setSessionBaseInfo: () => setSessionBaseInfo,
28945
28948
  switchSession: () => switchSession,
28946
28949
  undoHistory: () => undoHistory,
28947
28950
  updateHistoryPaths: () => updateHistoryPaths
28948
28951
  });
28949
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
28950
- import { join as join3, resolve as resolve3, sep } from "node:path";
28951
- import { homedir as homedir3, platform as platform2 } from "node:os";
28952
- import { randomUUID as randomUUID2 } from "node:crypto";
28952
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, rmSync } from "node:fs";
28953
+ import { basename as basename3, dirname as dirname2, extname, join as join2, resolve as resolve2, sep } from "node:path";
28954
+ import { homedir as homedir2, platform as platform2 } from "node:os";
28955
+ import { randomUUID } from "node:crypto";
28953
28956
  function globalHistoryDir() {
28954
28957
  if (platform2() === "win32") {
28955
- return join3(process.env.APPDATA || join3(homedir3(), "AppData", "Roaming"), "imgx");
28958
+ return join2(process.env.APPDATA || join2(homedir2(), "AppData", "Roaming"), "imgx");
28956
28959
  }
28957
- return join3(process.env.XDG_CONFIG_HOME || join3(homedir3(), ".config"), "imgx");
28960
+ return join2(process.env.XDG_CONFIG_HOME || join2(homedir2(), ".config"), "imgx");
28958
28961
  }
28959
28962
  function historyDir() {
28960
28963
  if (process.env.IMGX_TEST_CONFIG_DIR)
28961
28964
  return process.env.IMGX_TEST_CONFIG_DIR;
28962
28965
  const projectRoot = findProjectRoot();
28963
28966
  if (projectRoot)
28964
- return join3(projectRoot, ".imgx");
28967
+ return join2(projectRoot, ".imgx");
28965
28968
  return globalHistoryDir();
28966
28969
  }
28967
28970
  function historyPath() {
28968
- return join3(historyDir(), HISTORY_FILE);
28971
+ return join2(historyDir(), HISTORY_FILE);
28969
28972
  }
28970
28973
  function emptyHistory() {
28971
28974
  return {
@@ -28976,7 +28979,7 @@ function emptyHistory() {
28976
28979
  }
28977
28980
  function loadHistory() {
28978
28981
  try {
28979
- const raw = readFileSync4(historyPath(), "utf-8");
28982
+ const raw = readFileSync2(historyPath(), "utf-8");
28980
28983
  return JSON.parse(raw);
28981
28984
  } catch {
28982
28985
  return emptyHistory();
@@ -28984,12 +28987,12 @@ function loadHistory() {
28984
28987
  }
28985
28988
  function saveHistory(history) {
28986
28989
  const dir = historyDir();
28987
- mkdirSync3(dir, { recursive: true });
28988
- writeFileSync3(historyPath(), JSON.stringify(history, null, 2) + "\n", "utf-8");
28990
+ mkdirSync2(dir, { recursive: true });
28991
+ writeFileSync2(historyPath(), JSON.stringify(history, null, 2) + "\n", "utf-8");
28989
28992
  }
28990
28993
  function createSession() {
28991
28994
  return {
28992
- id: `s-${randomUUID2().slice(0, 8)}`,
28995
+ id: `s-${randomUUID().slice(0, 8)}`,
28993
28996
  entries: [],
28994
28997
  cursor: 0
28995
28998
  };
@@ -29002,6 +29005,13 @@ function pushHistory(entry, opts) {
29002
29005
  session2.id = opts.sessionId;
29003
29006
  if (opts.outputDir)
29004
29007
  session2.outputDir = opts.outputDir;
29008
+ if (entry.filePaths.length > 0) {
29009
+ const fp = entry.filePaths[0];
29010
+ const ext = extname(fp);
29011
+ session2.baseName = basename3(fp, ext);
29012
+ session2.baseExt = ext;
29013
+ session2.baseDir = dirname2(fp);
29014
+ }
29005
29015
  session2.entries.push(entry);
29006
29016
  history.sessions.push(session2);
29007
29017
  history.activeSessionId = session2.id;
@@ -29012,7 +29022,16 @@ function pushHistory(entry, opts) {
29012
29022
  if (!session)
29013
29023
  throw new Error("No active session. Run generate first.");
29014
29024
  if (session.cursor > 0) {
29015
- session.entries.splice(0, session.cursor);
29025
+ const removed = session.entries.splice(0, session.cursor);
29026
+ for (const entry2 of removed) {
29027
+ for (const fp of entry2.filePaths) {
29028
+ try {
29029
+ if (existsSync2(fp))
29030
+ rmSync(fp);
29031
+ } catch {
29032
+ }
29033
+ }
29034
+ }
29016
29035
  session.cursor = 0;
29017
29036
  }
29018
29037
  session.entries.unshift(entry);
@@ -29114,15 +29133,15 @@ function clearSession(sessionId) {
29114
29133
  return { filePaths };
29115
29134
  }
29116
29135
  function isManagedPath(filePath) {
29117
- const normalized = resolve3(filePath);
29118
- const projectManaged = resolve3(historyDir());
29119
- const globalManaged = resolve3(join3(homedir3(), "Pictures", "imgx"));
29136
+ const normalized = resolve2(filePath);
29137
+ const projectManaged = resolve2(historyDir());
29138
+ const globalManaged = resolve2(join2(homedir2(), "Pictures", "imgx"));
29120
29139
  return normalized.startsWith(projectManaged + sep) || normalized === projectManaged || normalized.startsWith(globalManaged + sep) || normalized === globalManaged;
29121
29140
  }
29122
29141
  function clearGlobalHistory() {
29123
- const globalPath = join3(globalHistoryDir(), HISTORY_FILE);
29142
+ const globalPath = join2(globalHistoryDir(), HISTORY_FILE);
29124
29143
  try {
29125
- const raw = readFileSync4(globalPath, "utf-8");
29144
+ const raw = readFileSync2(globalPath, "utf-8");
29126
29145
  const history = JSON.parse(raw);
29127
29146
  const filePaths = [];
29128
29147
  for (const session of history.sessions) {
@@ -29130,8 +29149,8 @@ function clearGlobalHistory() {
29130
29149
  filePaths.push(...entry.filePaths);
29131
29150
  }
29132
29151
  const dir = globalHistoryDir();
29133
- mkdirSync3(dir, { recursive: true });
29134
- writeFileSync3(globalPath, JSON.stringify(emptyHistory(), null, 2) + "\n", "utf-8");
29152
+ mkdirSync2(dir, { recursive: true });
29153
+ writeFileSync2(globalPath, JSON.stringify(emptyHistory(), null, 2) + "\n", "utf-8");
29135
29154
  return { filePaths };
29136
29155
  } catch {
29137
29156
  return { filePaths: [] };
@@ -29149,6 +29168,36 @@ function updateHistoryPaths(oldBase, newBase) {
29149
29168
  }
29150
29169
  saveHistory(history);
29151
29170
  }
29171
+ function getSessionChainNumber() {
29172
+ const history = loadHistory();
29173
+ if (!history.activeSessionId)
29174
+ return null;
29175
+ const session = history.sessions.find((s2) => s2.id === history.activeSessionId);
29176
+ if (!session || session.entries.length === 0)
29177
+ return null;
29178
+ return session.entries.length;
29179
+ }
29180
+ function getSessionBaseInfo() {
29181
+ const history = loadHistory();
29182
+ if (!history.activeSessionId)
29183
+ return null;
29184
+ const session = history.sessions.find((s2) => s2.id === history.activeSessionId);
29185
+ if (!session?.baseName)
29186
+ return null;
29187
+ return { baseName: session.baseName, baseExt: session.baseExt, baseDir: session.baseDir };
29188
+ }
29189
+ function setSessionBaseInfo(baseName, baseExt, baseDir) {
29190
+ const history = loadHistory();
29191
+ if (!history.activeSessionId)
29192
+ return;
29193
+ const session = history.sessions.find((s2) => s2.id === history.activeSessionId);
29194
+ if (!session)
29195
+ return;
29196
+ session.baseName = baseName;
29197
+ session.baseExt = baseExt;
29198
+ session.baseDir = baseDir;
29199
+ saveHistory(history);
29200
+ }
29152
29201
  var HISTORY_FILE;
29153
29202
  var init_history = __esm({
29154
29203
  "build/core/history.js"() {
@@ -69542,10 +69591,11 @@ function getApiKeyFromEnv() {
69542
69591
 
69543
69592
  // build/core/storage.js
69544
69593
  init_config();
69545
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "node:fs";
69546
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "node:path";
69547
- import { randomUUID } from "node:crypto";
69548
- import { homedir as homedir2 } from "node:os";
69594
+ init_history();
69595
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
69596
+ import { dirname as dirname3, join as join3, resolve as resolve3 } from "node:path";
69597
+ import { randomUUID as randomUUID2 } from "node:crypto";
69598
+ import { homedir as homedir3 } from "node:os";
69549
69599
  var MIME_TO_EXT = {
69550
69600
  "image/png": ".png",
69551
69601
  "image/jpeg": ".jpg",
@@ -69553,7 +69603,7 @@ var MIME_TO_EXT = {
69553
69603
  };
69554
69604
  function readImageAsBase64(filePath) {
69555
69605
  const absPath = resolveProjectPath(filePath);
69556
- const buffer = readFileSync2(absPath);
69606
+ const buffer = readFileSync3(absPath);
69557
69607
  const ext = filePath.split(".").pop()?.toLowerCase();
69558
69608
  const mimeType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "webp" ? "image/webp" : "image/png";
69559
69609
  return { data: buffer.toString("base64"), mimeType };
@@ -69566,21 +69616,29 @@ function fallbackOutputDir(outputDir) {
69566
69616
  return resolveProjectPath(configured);
69567
69617
  const projectRoot = findProjectRoot();
69568
69618
  if (projectRoot)
69569
- return join2(projectRoot, ".imgx");
69570
- return join2(homedir2(), "Pictures", "imgx");
69619
+ return join3(projectRoot, ".imgx");
69620
+ return join3(homedir3(), "Pictures", "imgx");
69571
69621
  }
69572
- function saveImage(image, outputPath, outputDir, sessionId) {
69622
+ function saveImage(image, outputPath, outputDir, sessionId, isChained) {
69573
69623
  const ext = MIME_TO_EXT[image.mimeType] || ".png";
69574
69624
  let filePath;
69575
- if (outputPath) {
69625
+ if (isChained) {
69626
+ const baseInfo = getSessionBaseInfo();
69627
+ const chainNum = getSessionChainNumber();
69628
+ if (baseInfo && chainNum !== null) {
69629
+ filePath = resolve3(baseInfo.baseDir, `${baseInfo.baseName}-${chainNum}${baseInfo.baseExt}`);
69630
+ } else {
69631
+ filePath = resolve3(fallbackOutputDir(outputDir), sessionId || "", `imgx-${randomUUID2().slice(0, 8)}${ext}`);
69632
+ }
69633
+ } else if (outputPath) {
69576
69634
  filePath = resolveProjectPath(outputPath);
69577
69635
  } else {
69578
69636
  const baseDir = fallbackOutputDir(outputDir);
69579
- const targetDir = sessionId ? join2(baseDir, sessionId) : baseDir;
69580
- filePath = resolve2(targetDir, `imgx-${randomUUID().slice(0, 8)}${ext}`);
69637
+ const targetDir = sessionId ? join3(baseDir, sessionId) : baseDir;
69638
+ filePath = resolve3(targetDir, `imgx-${randomUUID2().slice(0, 8)}${ext}`);
69581
69639
  }
69582
- mkdirSync2(dirname2(filePath), { recursive: true });
69583
- writeFileSync2(filePath, image.data);
69640
+ mkdirSync3(dirname3(filePath), { recursive: true });
69641
+ writeFileSync3(filePath, image.data);
69584
69642
  return filePath;
69585
69643
  }
69586
69644
 
@@ -69719,7 +69777,7 @@ init_config();
69719
69777
 
69720
69778
  // build/providers/openai/client.js
69721
69779
  init_config();
69722
- import { readFileSync as readFileSync3 } from "node:fs";
69780
+ import { readFileSync as readFileSync4 } from "node:fs";
69723
69781
 
69724
69782
  // build/providers/openai/capabilities.js
69725
69783
  var OPENAI_PROVIDER_INFO = {
@@ -69833,7 +69891,7 @@ var OpenAIProvider = class {
69833
69891
  async edit(input, model) {
69834
69892
  const modelName = model || this.info.defaultModel;
69835
69893
  const absPath = resolveProjectPath(input.inputImage);
69836
- const imageBuffer = readFileSync3(absPath);
69894
+ const imageBuffer = readFileSync4(absPath);
69837
69895
  const ext = absPath.split(".").pop()?.toLowerCase();
69838
69896
  const contentType = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : ext === "webp" ? "image/webp" : "image/png";
69839
69897
  const fields = {
@@ -69919,7 +69977,7 @@ function buildImageContent(images, paths, extra) {
69919
69977
  }
69920
69978
  var server = new McpServer({
69921
69979
  name: "imgx",
69922
- version: "1.3.0"
69980
+ version: "1.4.0"
69923
69981
  });
69924
69982
  initGemini();
69925
69983
  initOpenAI();
@@ -70060,7 +70118,7 @@ server.tool("edit_last", "Edit the last generated/edited image with new text ins
70060
70118
  }
70061
70119
  const sessionId = loadHistory().activeSessionId;
70062
70120
  const sessionOutputDir = args.output_dir || getActiveSessionOutputDir();
70063
- const saved = saveImage(result.images[0], args.output, sessionOutputDir, sessionId || void 0);
70121
+ const saved = saveImage(result.images[0], args.output, sessionOutputDir, sessionId || void 0, true);
70064
70122
  pushHistory({
70065
70123
  filePaths: [saved],
70066
70124
  prompt: args.prompt,
@@ -70142,8 +70200,8 @@ server.tool("clear_history", "Clear edit history for the current project. Files
70142
70200
  let filesDeleted = 0;
70143
70201
  let skippedFiles = 0;
70144
70202
  if (args.delete_files) {
70145
- const { existsSync: existsSync2, rmSync, rmdirSync } = await import("node:fs");
70146
- const { dirname: dirname3 } = await import("node:path");
70203
+ const { existsSync: existsSync3, rmSync: rmSync2, rmdirSync } = await import("node:fs");
70204
+ const { dirname: dirname4 } = await import("node:path");
70147
70205
  const dirs = /* @__PURE__ */ new Set();
70148
70206
  for (const fp of result.filePaths) {
70149
70207
  if (!isManagedPath(fp)) {
@@ -70151,10 +70209,10 @@ server.tool("clear_history", "Clear edit history for the current project. Files
70151
70209
  continue;
70152
70210
  }
70153
70211
  try {
70154
- if (existsSync2(fp)) {
70155
- rmSync(fp);
70212
+ if (existsSync3(fp)) {
70213
+ rmSync2(fp);
70156
70214
  filesDeleted++;
70157
- dirs.add(dirname3(fp));
70215
+ dirs.add(dirname4(fp));
70158
70216
  }
70159
70217
  } catch {
70160
70218
  }
@@ -70187,11 +70245,11 @@ server.tool("set_output_dir", "Change the default output directory for generated
70187
70245
  config2.defaults.outputDir = args.path;
70188
70246
  saveConfig2(config2);
70189
70247
  if (args.move_files && oldDir !== args.path) {
70190
- const { mkdirSync: mkdirSync4, cpSync, rmSync, existsSync: existsSync2 } = await import("node:fs");
70191
- if (existsSync2(oldDir)) {
70248
+ const { mkdirSync: mkdirSync4, cpSync, rmSync: rmSync2, existsSync: existsSync3 } = await import("node:fs");
70249
+ if (existsSync3(oldDir)) {
70192
70250
  mkdirSync4(args.path, { recursive: true });
70193
70251
  cpSync(oldDir, args.path, { recursive: true });
70194
- rmSync(oldDir, { recursive: true, force: true });
70252
+ rmSync2(oldDir, { recursive: true, force: true });
70195
70253
  updateHistoryPaths2(oldDir, args.path);
70196
70254
  }
70197
70255
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imgx-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "mcpName": "io.github.somacoffeekyoto/imgx",
5
5
  "description": "AI image generation and editing for Claude Code, Codex CLI, and MCP-compatible AI agents",
6
6
  "type": "module",