@mobvibe/cli 0.1.6 → 0.1.7

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/index.js CHANGED
@@ -380,7 +380,7 @@ var getCliConfig = async () => {
380
380
  // src/daemon/daemon.ts
381
381
  import { spawn as spawn2 } from "child_process";
382
382
  import fs5 from "fs/promises";
383
- import path5 from "path";
383
+ import path6 from "path";
384
384
 
385
385
  // src/acp/session-manager.ts
386
386
  import { randomUUID as randomUUID2 } from "crypto";
@@ -1045,12 +1045,12 @@ var SessionManager = class {
1045
1045
  emitSessionsChanged(payload) {
1046
1046
  this.sessionsChangedEmitter.emit("changed", payload);
1047
1047
  }
1048
- emitSessionAttached(sessionId) {
1048
+ emitSessionAttached(sessionId, force = false) {
1049
1049
  const record = this.sessions.get(sessionId);
1050
1050
  if (!record) {
1051
1051
  return;
1052
1052
  }
1053
- if (record.isAttached) {
1053
+ if (record.isAttached && !force) {
1054
1054
  return;
1055
1055
  }
1056
1056
  const attachedAt = /* @__PURE__ */ new Date();
@@ -1497,7 +1497,7 @@ var SessionManager = class {
1497
1497
  async loadSession(sessionId, cwd, backendId) {
1498
1498
  const existing = this.sessions.get(sessionId);
1499
1499
  if (existing) {
1500
- this.emitSessionAttached(sessionId);
1500
+ this.emitSessionAttached(sessionId, true);
1501
1501
  return this.buildSummary(existing);
1502
1502
  }
1503
1503
  const backend = this.resolveBackend(backendId);
@@ -1580,6 +1580,47 @@ var SessionManager = class {
1580
1580
  throw error;
1581
1581
  }
1582
1582
  }
1583
+ /**
1584
+ * Reload a historical session from the ACP agent.
1585
+ * Replays session history even if the session is already loaded.
1586
+ */
1587
+ async reloadSession(sessionId, cwd, backendId) {
1588
+ const existing = this.sessions.get(sessionId);
1589
+ if (!existing) {
1590
+ return this.loadSession(sessionId, cwd, backendId);
1591
+ }
1592
+ if (!existing.connection.supportsSessionLoad()) {
1593
+ throw createCapabilityNotSupportedError(
1594
+ "Agent does not support session loading"
1595
+ );
1596
+ }
1597
+ const response = await existing.connection.loadSession(sessionId, cwd);
1598
+ const { modelId, modelName, availableModels } = resolveModelState(
1599
+ response.models
1600
+ );
1601
+ const { modeId, modeName, availableModes } = resolveModeState(
1602
+ response.modes
1603
+ );
1604
+ const agentInfo = existing.connection.getAgentInfo();
1605
+ existing.cwd = cwd;
1606
+ existing.agentName = agentInfo?.title ?? agentInfo?.name ?? existing.agentName;
1607
+ existing.modelId = modelId;
1608
+ existing.modelName = modelName;
1609
+ existing.availableModels = availableModels;
1610
+ existing.modeId = modeId;
1611
+ existing.modeName = modeName;
1612
+ existing.availableModes = availableModes;
1613
+ existing.updatedAt = /* @__PURE__ */ new Date();
1614
+ const summary = this.buildSummary(existing);
1615
+ this.emitSessionsChanged({
1616
+ added: [],
1617
+ updated: [summary],
1618
+ removed: []
1619
+ });
1620
+ this.emitSessionAttached(sessionId, true);
1621
+ logger.info({ sessionId, backendId }, "session_reloaded");
1622
+ return summary;
1623
+ }
1583
1624
  applySessionUpdateToRecord(record, notification) {
1584
1625
  const update = notification.update;
1585
1626
  if (update.sessionUpdate === "current_mode_update") {
@@ -1656,9 +1697,187 @@ var SessionManager = class {
1656
1697
  import { EventEmitter as EventEmitter3 } from "events";
1657
1698
  import fs4 from "fs/promises";
1658
1699
  import { homedir } from "os";
1659
- import path4 from "path";
1700
+ import path5 from "path";
1660
1701
  import ignore from "ignore";
1661
1702
  import { io } from "socket.io-client";
1703
+
1704
+ // src/lib/git-utils.ts
1705
+ import { exec } from "child_process";
1706
+ import path4 from "path";
1707
+ import { promisify } from "util";
1708
+ var execAsync = promisify(exec);
1709
+ var MAX_BUFFER = 10 * 1024 * 1024;
1710
+ async function isGitRepo(cwd) {
1711
+ try {
1712
+ await execAsync("git rev-parse --is-inside-work-tree", {
1713
+ cwd,
1714
+ maxBuffer: MAX_BUFFER
1715
+ });
1716
+ return true;
1717
+ } catch {
1718
+ return false;
1719
+ }
1720
+ }
1721
+ async function getGitBranch(cwd) {
1722
+ try {
1723
+ const { stdout } = await execAsync("git branch --show-current", {
1724
+ cwd,
1725
+ maxBuffer: MAX_BUFFER
1726
+ });
1727
+ const branch = stdout.trim();
1728
+ if (branch) {
1729
+ return branch;
1730
+ }
1731
+ const { stdout: hashOut } = await execAsync("git rev-parse --short HEAD", {
1732
+ cwd,
1733
+ maxBuffer: MAX_BUFFER
1734
+ });
1735
+ return hashOut.trim() || void 0;
1736
+ } catch {
1737
+ return void 0;
1738
+ }
1739
+ }
1740
+ function parseGitStatus(output) {
1741
+ const files = [];
1742
+ const lines = output.split("\n").filter((line) => line.length > 0);
1743
+ for (const line of lines) {
1744
+ const indexStatus = line[0];
1745
+ const workTreeStatus = line[1];
1746
+ const filePath = line.slice(3).split(" -> ").pop()?.trim();
1747
+ if (!filePath) {
1748
+ continue;
1749
+ }
1750
+ let status;
1751
+ if (indexStatus === "?" || workTreeStatus === "?") {
1752
+ status = "?";
1753
+ } else if (indexStatus === "!" || workTreeStatus === "!") {
1754
+ status = "!";
1755
+ } else if (indexStatus === "A" || workTreeStatus === "A") {
1756
+ status = "A";
1757
+ } else if (indexStatus === "D" || workTreeStatus === "D") {
1758
+ status = "D";
1759
+ } else if (indexStatus === "R" || workTreeStatus === "R") {
1760
+ status = "R";
1761
+ } else if (indexStatus === "C" || workTreeStatus === "C") {
1762
+ status = "C";
1763
+ } else if (indexStatus === "U" || workTreeStatus === "U") {
1764
+ status = "U";
1765
+ } else if (indexStatus === "M" || workTreeStatus === "M" || indexStatus !== " " || workTreeStatus !== " ") {
1766
+ status = "M";
1767
+ } else {
1768
+ continue;
1769
+ }
1770
+ files.push({ path: filePath, status });
1771
+ }
1772
+ return files;
1773
+ }
1774
+ async function getGitStatus(cwd) {
1775
+ try {
1776
+ const { stdout } = await execAsync("git status --porcelain=v1", {
1777
+ cwd,
1778
+ maxBuffer: MAX_BUFFER
1779
+ });
1780
+ return parseGitStatus(stdout);
1781
+ } catch {
1782
+ return [];
1783
+ }
1784
+ }
1785
+ function aggregateDirStatus(files) {
1786
+ const dirStatus = {};
1787
+ const statusPriority = {
1788
+ A: 7,
1789
+ D: 6,
1790
+ M: 5,
1791
+ R: 4,
1792
+ C: 3,
1793
+ U: 2,
1794
+ "?": 1,
1795
+ "!": 0
1796
+ };
1797
+ for (const file of files) {
1798
+ const parts = file.path.split("/");
1799
+ let currentPath = "";
1800
+ for (let i = 0; i < parts.length - 1; i++) {
1801
+ currentPath = currentPath ? `${currentPath}/${parts[i]}` : parts[i];
1802
+ const existing = dirStatus[currentPath];
1803
+ if (!existing || statusPriority[file.status] > statusPriority[existing]) {
1804
+ dirStatus[currentPath] = file.status;
1805
+ }
1806
+ }
1807
+ }
1808
+ return dirStatus;
1809
+ }
1810
+ function parseDiffOutput(diffOutput) {
1811
+ const addedLines = [];
1812
+ const modifiedLines = [];
1813
+ const deletedLines = [];
1814
+ const lines = diffOutput.split("\n");
1815
+ let currentLine = 0;
1816
+ let inHunk = false;
1817
+ let pendingDeletionLine = 0;
1818
+ for (const line of lines) {
1819
+ const hunkMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
1820
+ if (hunkMatch) {
1821
+ currentLine = Number.parseInt(hunkMatch[1], 10);
1822
+ inHunk = true;
1823
+ pendingDeletionLine = 0;
1824
+ continue;
1825
+ }
1826
+ if (!inHunk) {
1827
+ continue;
1828
+ }
1829
+ if (line.startsWith("+") && !line.startsWith("+++")) {
1830
+ addedLines.push(currentLine);
1831
+ currentLine++;
1832
+ pendingDeletionLine = 0;
1833
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
1834
+ if (pendingDeletionLine === 0) {
1835
+ pendingDeletionLine = currentLine;
1836
+ }
1837
+ const deletionPos = Math.max(1, currentLine);
1838
+ if (!deletedLines.includes(deletionPos)) {
1839
+ deletedLines.push(deletionPos);
1840
+ }
1841
+ } else if (!line.startsWith("\\")) {
1842
+ currentLine++;
1843
+ pendingDeletionLine = 0;
1844
+ }
1845
+ }
1846
+ return { addedLines, modifiedLines, deletedLines };
1847
+ }
1848
+ async function getFileDiff(cwd, filePath) {
1849
+ try {
1850
+ const relativePath = path4.isAbsolute(filePath) ? path4.relative(cwd, filePath) : filePath;
1851
+ const { stdout } = await execAsync(`git diff HEAD -- "${relativePath}"`, {
1852
+ cwd,
1853
+ maxBuffer: MAX_BUFFER
1854
+ });
1855
+ if (!stdout.trim()) {
1856
+ const { stdout: statusOut } = await execAsync(
1857
+ `git status --porcelain=v1 -- "${relativePath}"`,
1858
+ { cwd, maxBuffer: MAX_BUFFER }
1859
+ );
1860
+ if (statusOut.startsWith("?") || statusOut.startsWith("A")) {
1861
+ const { stdout: wcOut } = await execAsync(`wc -l < "${relativePath}"`, {
1862
+ cwd,
1863
+ maxBuffer: MAX_BUFFER
1864
+ });
1865
+ const lineCount = Number.parseInt(wcOut.trim(), 10) || 0;
1866
+ return {
1867
+ addedLines: Array.from({ length: lineCount }, (_, i) => i + 1),
1868
+ modifiedLines: [],
1869
+ deletedLines: []
1870
+ };
1871
+ }
1872
+ return { addedLines: [], modifiedLines: [], deletedLines: [] };
1873
+ }
1874
+ return parseDiffOutput(stdout);
1875
+ } catch {
1876
+ return { addedLines: [], modifiedLines: [], deletedLines: [] };
1877
+ }
1878
+ }
1879
+
1880
+ // src/daemon/socket-client.ts
1662
1881
  var SESSION_ROOT_NAME = "Working Directory";
1663
1882
  var MAX_RESOURCE_FILES = 2e3;
1664
1883
  var DEFAULT_IGNORES = [
@@ -1678,7 +1897,7 @@ var DEFAULT_IGNORES = [
1678
1897
  var loadGitignore = async (rootPath) => {
1679
1898
  const ig = ignore().add(DEFAULT_IGNORES);
1680
1899
  try {
1681
- const gitignorePath = path4.join(rootPath, ".gitignore");
1900
+ const gitignorePath = path5.join(rootPath, ".gitignore");
1682
1901
  const content = await fs4.readFile(gitignorePath, "utf8");
1683
1902
  ig.add(content);
1684
1903
  } catch {
@@ -1686,7 +1905,7 @@ var loadGitignore = async (rootPath) => {
1686
1905
  return ig;
1687
1906
  };
1688
1907
  var resolveImageMimeType = (filePath) => {
1689
- const extension = path4.extname(filePath).toLowerCase();
1908
+ const extension = path5.extname(filePath).toLowerCase();
1690
1909
  switch (extension) {
1691
1910
  case ".apng":
1692
1911
  return "image/apng";
@@ -1712,7 +1931,7 @@ var readDirectoryEntries = async (dirPath) => {
1712
1931
  const entries = await fs4.readdir(dirPath, { withFileTypes: true });
1713
1932
  const resolvedEntries = await Promise.all(
1714
1933
  entries.map(async (entry) => {
1715
- const entryPath = path4.join(dirPath, entry.name);
1934
+ const entryPath = path5.join(dirPath, entry.name);
1716
1935
  let isDirectory = entry.isDirectory();
1717
1936
  if (!isDirectory && entry.isSymbolicLink()) {
1718
1937
  try {
@@ -2112,7 +2331,7 @@ var SocketClient = class extends EventEmitter3 {
2112
2331
  if (!record || !record.cwd) {
2113
2332
  throw new Error("Session not found or no working directory");
2114
2333
  }
2115
- const resolved = requestPath ? path4.isAbsolute(requestPath) ? requestPath : path4.join(record.cwd, requestPath) : record.cwd;
2334
+ const resolved = requestPath ? path5.isAbsolute(requestPath) ? requestPath : path5.join(record.cwd, requestPath) : record.cwd;
2116
2335
  const entries = await readDirectoryEntries(resolved);
2117
2336
  this.sendRpcResponse(request.requestId, { path: resolved, entries });
2118
2337
  } catch (error) {
@@ -2138,7 +2357,7 @@ var SocketClient = class extends EventEmitter3 {
2138
2357
  if (!record || !record.cwd) {
2139
2358
  throw new Error("Session not found or no working directory");
2140
2359
  }
2141
- const resolved = path4.isAbsolute(requestPath) ? requestPath : path4.join(record.cwd, requestPath);
2360
+ const resolved = path5.isAbsolute(requestPath) ? requestPath : path5.join(record.cwd, requestPath);
2142
2361
  const mimeType = resolveImageMimeType(resolved);
2143
2362
  if (mimeType) {
2144
2363
  const buffer = await fs4.readFile(resolved);
@@ -2243,6 +2462,113 @@ var SocketClient = class extends EventEmitter3 {
2243
2462
  this.sendRpcError(request.requestId, error);
2244
2463
  }
2245
2464
  });
2465
+ this.socket.on("rpc:session:reload", async (request) => {
2466
+ try {
2467
+ const { sessionId, cwd } = request.params;
2468
+ logger.info(
2469
+ { requestId: request.requestId, sessionId, cwd },
2470
+ "rpc_session_reload"
2471
+ );
2472
+ const session = await sessionManager.reloadSession(sessionId, cwd);
2473
+ this.sendRpcResponse(request.requestId, session);
2474
+ } catch (error) {
2475
+ logger.error(
2476
+ {
2477
+ err: error,
2478
+ requestId: request.requestId,
2479
+ sessionId: request.params.sessionId
2480
+ },
2481
+ "rpc_session_reload_error"
2482
+ );
2483
+ this.sendRpcError(request.requestId, error);
2484
+ }
2485
+ });
2486
+ this.socket.on("rpc:git:status", async (request) => {
2487
+ try {
2488
+ const { sessionId } = request.params;
2489
+ logger.debug(
2490
+ { requestId: request.requestId, sessionId },
2491
+ "rpc_git_status"
2492
+ );
2493
+ const record = sessionManager.getSession(sessionId);
2494
+ if (!record || !record.cwd) {
2495
+ throw new Error("Session not found or no working directory");
2496
+ }
2497
+ const isRepo = await isGitRepo(record.cwd);
2498
+ if (!isRepo) {
2499
+ this.sendRpcResponse(request.requestId, {
2500
+ isGitRepo: false,
2501
+ files: [],
2502
+ dirStatus: {}
2503
+ });
2504
+ return;
2505
+ }
2506
+ const [branch, files] = await Promise.all([
2507
+ getGitBranch(record.cwd),
2508
+ getGitStatus(record.cwd)
2509
+ ]);
2510
+ const dirStatus = aggregateDirStatus(files);
2511
+ this.sendRpcResponse(request.requestId, {
2512
+ isGitRepo: true,
2513
+ branch,
2514
+ files,
2515
+ dirStatus
2516
+ });
2517
+ } catch (error) {
2518
+ logger.error(
2519
+ {
2520
+ err: error,
2521
+ requestId: request.requestId,
2522
+ sessionId: request.params.sessionId
2523
+ },
2524
+ "rpc_git_status_error"
2525
+ );
2526
+ this.sendRpcError(request.requestId, error);
2527
+ }
2528
+ });
2529
+ this.socket.on("rpc:git:fileDiff", async (request) => {
2530
+ try {
2531
+ const { sessionId, path: filePath } = request.params;
2532
+ logger.debug(
2533
+ { requestId: request.requestId, sessionId, path: filePath },
2534
+ "rpc_git_file_diff"
2535
+ );
2536
+ const record = sessionManager.getSession(sessionId);
2537
+ if (!record || !record.cwd) {
2538
+ throw new Error("Session not found or no working directory");
2539
+ }
2540
+ const isRepo = await isGitRepo(record.cwd);
2541
+ if (!isRepo) {
2542
+ this.sendRpcResponse(request.requestId, {
2543
+ isGitRepo: false,
2544
+ path: filePath,
2545
+ addedLines: [],
2546
+ modifiedLines: []
2547
+ });
2548
+ return;
2549
+ }
2550
+ const { addedLines, modifiedLines } = await getFileDiff(
2551
+ record.cwd,
2552
+ filePath
2553
+ );
2554
+ this.sendRpcResponse(request.requestId, {
2555
+ isGitRepo: true,
2556
+ path: filePath,
2557
+ addedLines,
2558
+ modifiedLines
2559
+ });
2560
+ } catch (error) {
2561
+ logger.error(
2562
+ {
2563
+ err: error,
2564
+ requestId: request.requestId,
2565
+ sessionId: request.params.sessionId
2566
+ },
2567
+ "rpc_git_file_diff_error"
2568
+ );
2569
+ this.sendRpcError(request.requestId, error);
2570
+ }
2571
+ });
2246
2572
  }
2247
2573
  setupSessionManagerListeners() {
2248
2574
  const { sessionManager } = this.options;
@@ -2302,8 +2628,8 @@ var SocketClient = class extends EventEmitter3 {
2302
2628
  const ig = await loadGitignore(rootPath);
2303
2629
  const allFiles = await this.listAllFiles(rootPath, ig, rootPath, []);
2304
2630
  return allFiles.map((filePath) => ({
2305
- name: path4.basename(filePath),
2306
- relativePath: path4.relative(rootPath, filePath),
2631
+ name: path5.basename(filePath),
2632
+ relativePath: path5.relative(rootPath, filePath),
2307
2633
  path: filePath
2308
2634
  }));
2309
2635
  }
@@ -2316,8 +2642,8 @@ var SocketClient = class extends EventEmitter3 {
2316
2642
  if (collected.length >= MAX_RESOURCE_FILES) {
2317
2643
  break;
2318
2644
  }
2319
- const entryPath = path4.join(rootPath, entry.name);
2320
- const relativePath = path4.relative(baseDir, entryPath);
2645
+ const entryPath = path5.join(rootPath, entry.name);
2646
+ const relativePath = path5.relative(baseDir, entryPath);
2321
2647
  const checkPath = entry.isDirectory() ? `${relativePath}/` : relativePath;
2322
2648
  if (ig.ignores(checkPath)) {
2323
2649
  continue;
@@ -2504,7 +2830,7 @@ var DaemonManager = class {
2504
2830
  }
2505
2831
  }
2506
2832
  async spawnBackground() {
2507
- const logFile = path5.join(
2833
+ const logFile = path6.join(
2508
2834
  this.config.logPath,
2509
2835
  `${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}-daemon.log`
2510
2836
  );
@@ -2605,7 +2931,7 @@ var DaemonManager = class {
2605
2931
  console.log("No log files found");
2606
2932
  return;
2607
2933
  }
2608
- const latestLog = path5.join(this.config.logPath, logFiles[0]);
2934
+ const latestLog = path6.join(this.config.logPath, logFiles[0]);
2609
2935
  logger.info({ logFile: latestLog }, "daemon_logs_latest");
2610
2936
  console.log(`Log file: ${latestLog}
2611
2937
  `);