agentgui 1.0.236 → 1.0.237

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/bin/gmgui.cjs CHANGED
@@ -16,19 +16,29 @@ async function gmgui(args = []) {
16
16
  const installer = 'npm';
17
17
 
18
18
  // Ensure dependencies are installed only if node_modules is missing
19
- // Skip this for bunx which manages dependencies independently
19
+ // Skip this for bunx/npx which manage dependencies independently
20
20
  const nodeModulesPath = path.join(projectRoot, 'node_modules');
21
- const isBunx = process.env.npm_execpath && process.env.npm_execpath.includes('bunx');
21
+ const execPath = process.env.npm_execpath || '';
22
+ const isBunx = execPath.includes('bun') || process.env.BUN_INSTALL;
23
+ const isNpx = execPath.includes('npx') || process.env._.includes('npx');
24
+
25
+ // Also skip if running from temp/cache directory (bunx/npm cache)
26
+ const isFromCache = projectRoot.includes('node_modules') &&
27
+ (projectRoot.includes('.bun') || projectRoot.includes('_npx') || projectRoot.includes('npm-cache'));
22
28
 
23
- if (!isBunx && !fs.existsSync(nodeModulesPath)) {
29
+ if (!isBunx && !isNpx && !isFromCache && !fs.existsSync(nodeModulesPath)) {
24
30
  console.log(`Installing dependencies with ${installer}...`);
25
31
  const installResult = spawnSync(installer, ['install'], {
26
32
  cwd: projectRoot,
27
- stdio: 'inherit'
33
+ stdio: 'inherit',
34
+ shell: true
28
35
  });
29
- if (installResult.status !== 0) {
36
+ if (installResult.status !== 0 && installResult.status !== null) {
30
37
  throw new Error(`${installer} install failed with code ${installResult.status}`);
31
38
  }
39
+ if (installResult.error) {
40
+ throw new Error(`${installer} install failed: ${installResult.error.message}`);
41
+ }
32
42
  }
33
43
 
34
44
  const port = process.env.PORT || 3000;
@@ -1,5 +1,15 @@
1
1
  import { spawn } from 'child_process';
2
2
 
3
+ const isWindows = process.platform === 'win32';
4
+
5
+ function getSpawnOptions(cwd, additionalOptions = {}) {
6
+ const options = { cwd, ...additionalOptions };
7
+ if (isWindows) {
8
+ options.shell = true;
9
+ }
10
+ return options;
11
+ }
12
+
3
13
  /**
4
14
  * Agent Framework
5
15
  * Extensible registry for AI agent CLI integrations
@@ -51,7 +61,7 @@ class AgentRunner {
51
61
  } = config;
52
62
 
53
63
  const args = this.buildArgs(prompt, config);
54
- const proc = spawn(this.command, args, { cwd });
64
+ const proc = spawn(this.command, args, getSpawnOptions(cwd));
55
65
 
56
66
  if (config.onPid) {
57
67
  try { config.onPid(proc.pid); } catch (e) {}
@@ -211,7 +221,7 @@ class AgentRunner {
211
221
  const baseArgs = this.requiresAdapter && this.adapterCommand ? this.adapterArgs : this.buildArgs(prompt, config);
212
222
  const args = [...baseArgs];
213
223
 
214
- const proc = spawn(cmd, args, { cwd });
224
+ const proc = spawn(cmd, args, getSpawnOptions(cwd));
215
225
 
216
226
  if (config.onPid) {
217
227
  try { config.onPid(proc.pid); } catch (e) {}
@@ -474,13 +484,15 @@ class AgentRegistry {
474
484
 
475
485
  listACPAvailable() {
476
486
  const { spawnSync } = require('child_process');
487
+ const isWindows = process.platform === 'win32';
477
488
  return this.list().filter(agent => {
478
489
  try {
479
- const which = spawnSync('which', [agent.command], { encoding: 'utf-8', timeout: 3000 });
490
+ const whichCmd = isWindows ? 'where' : 'which';
491
+ const which = spawnSync(whichCmd, [agent.command], { encoding: 'utf-8', timeout: 3000 });
480
492
  if (which.status !== 0) return false;
481
- const binPath = (which.stdout || '').trim();
493
+ const binPath = (which.stdout || '').trim().split('\n')[0].trim();
482
494
  if (!binPath) return false;
483
- const check = spawnSync(binPath, ['--version'], { encoding: 'utf-8', timeout: 10000 });
495
+ const check = spawnSync(binPath, ['--version'], { encoding: 'utf-8', timeout: 10000, shell: isWindows });
484
496
  return check.status === 0 && (check.stdout || '').trim().length > 0;
485
497
  } catch {
486
498
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.236",
3
+ "version": "1.0.237",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -1260,7 +1260,8 @@ const server = http.createServer(async (req, res) => {
1260
1260
  delete childEnv.PORT;
1261
1261
  delete childEnv.BASE_URL;
1262
1262
  delete childEnv.HOT_RELOAD;
1263
- const child = spawn('npm', ['run', script], { cwd: wd, stdio: ['ignore', 'pipe', 'pipe'], detached: true, env: childEnv });
1263
+ const isWindows = os.platform() === 'win32';
1264
+ const child = spawn('npm', ['run', script], { cwd: wd, stdio: ['ignore', 'pipe', 'pipe'], detached: true, env: childEnv, shell: isWindows });
1264
1265
  activeScripts.set(conversationId, { process: child, script, startTime: Date.now() });
1265
1266
  broadcastSync({ type: 'script_started', conversationId, script, timestamp: Date.now() });
1266
1267
 
@@ -1508,7 +1509,8 @@ const server = http.createServer(async (req, res) => {
1508
1509
 
1509
1510
  const child = spawn(authCmd.cmd, authCmd.args, {
1510
1511
  stdio: ['pipe', 'pipe', 'pipe'],
1511
- env: { ...process.env, FORCE_COLOR: '1' }
1512
+ env: { ...process.env, FORCE_COLOR: '1' },
1513
+ shell: os.platform() === 'win32'
1512
1514
  });
1513
1515
  activeScripts.set(conversationId, { process: child, script: 'auth-' + agentId, startTime: Date.now() });
1514
1516
  broadcastSync({ type: 'script_started', conversationId, script: 'auth-' + agentId, agentId, timestamp: Date.now() });
@@ -1721,12 +1723,14 @@ const server = http.createServer(async (req, res) => {
1721
1723
  return;
1722
1724
  }
1723
1725
  try {
1724
- execSync(`git clone https://github.com/${repo}.git`, {
1726
+ const isWindows = os.platform() === 'win32';
1727
+ execSync('git clone https://github.com/' + repo + '.git', {
1725
1728
  cwd: cloneDir,
1726
1729
  encoding: 'utf-8',
1727
1730
  timeout: 120000,
1728
1731
  stdio: ['pipe', 'pipe', 'pipe'],
1729
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }
1732
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
1733
+ shell: isWindows
1730
1734
  });
1731
1735
  sendJSON(req, res, 200, { ok: true, repo, path: targetPath, name: repoName });
1732
1736
  } catch (err) {
@@ -1756,7 +1760,8 @@ const server = http.createServer(async (req, res) => {
1756
1760
 
1757
1761
  if (pathOnly === '/api/git/check-remote-ownership' && req.method === 'GET') {
1758
1762
  try {
1759
- const result = execSync('git remote get-url origin 2>/dev/null', { encoding: 'utf-8', cwd: STARTUP_CWD });
1763
+ const isWindows = os.platform() === 'win32';
1764
+ const result = execSync('git remote get-url origin' + (isWindows ? '' : ' 2>/dev/null'), { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
1760
1765
  const remoteUrl = result.trim();
1761
1766
  const statusResult = execSync('git status --porcelain', { encoding: 'utf-8', cwd: STARTUP_CWD });
1762
1767
  const hasChanges = statusResult.trim().length > 0;
@@ -1770,7 +1775,8 @@ const server = http.createServer(async (req, res) => {
1770
1775
 
1771
1776
  if (pathOnly === '/api/git/push' && req.method === 'POST') {
1772
1777
  try {
1773
- execSync('git add -A && git commit -m "Auto-commit" && git push', { encoding: 'utf-8', cwd: STARTUP_CWD });
1778
+ const isWindows = os.platform() === 'win32';
1779
+ execSync('git add -A && git commit -m "Auto-commit" && git push', { encoding: 'utf-8', cwd: STARTUP_CWD, shell: isWindows });
1774
1780
  sendJSON(req, res, 200, { success: true });
1775
1781
  } catch (err) {
1776
1782
  sendJSON(req, res, 500, { error: err.message });
@@ -1784,7 +1790,9 @@ const server = http.createServer(async (req, res) => {
1784
1790
  const expandedPath = decodedPath.startsWith('~') ?
1785
1791
  decodedPath.replace('~', os.homedir()) : decodedPath;
1786
1792
  const normalizedPath = path.normalize(expandedPath);
1787
- if (!normalizedPath.startsWith('/') || normalizedPath.includes('..')) {
1793
+ const isWindows = os.platform() === 'win32';
1794
+ const isAbsolute = isWindows ? /^[A-Za-z]:[\\\/]/.test(normalizedPath) : normalizedPath.startsWith('/');
1795
+ if (!isAbsolute || normalizedPath.includes('..')) {
1788
1796
  res.writeHead(403); res.end('Forbidden'); return;
1789
1797
  }
1790
1798
  try {
@@ -4,6 +4,15 @@
4
4
  * Includes folder browser for selecting working directory on new conversation
5
5
  */
6
6
 
7
+ function pathSplit(p) {
8
+ return p.split(/[\/\\]/).filter(Boolean);
9
+ }
10
+
11
+ function pathBasename(p) {
12
+ const parts = pathSplit(p);
13
+ return parts.length ? parts.pop() : '';
14
+ }
15
+
7
16
  class ConversationManager {
8
17
  constructor() {
9
18
  this.conversations = [];
@@ -176,7 +185,8 @@ class ConversationManager {
176
185
  li.innerHTML = `<span class="folder-list-item-icon">&#128193;</span><span class="folder-list-item-name">${this.escapeHtml(folder.name)}</span>`;
177
186
  li.addEventListener('click', () => {
178
187
  const expandedBase = dirPath === '~' ? this.folderBrowser.homePath : dirPath;
179
- const newPath = expandedBase + '/' + folder.name;
188
+ const separator = expandedBase.includes('\\') ? '\\' : '/';
189
+ const newPath = expandedBase + separator + folder.name;
180
190
  this.loadFolders(newPath);
181
191
  });
182
192
  this.folderBrowser.listEl.appendChild(li);
@@ -189,26 +199,31 @@ class ConversationManager {
189
199
 
190
200
  getParentPath(dirPath) {
191
201
  const expanded = dirPath === '~' ? this.folderBrowser.homePath : dirPath;
192
- const parts = expanded.split('/').filter(Boolean);
193
- if (parts.length <= 1) return '/';
202
+ const parts = pathSplit(expanded);
203
+ if (parts.length <= 1) {
204
+ const separator = expanded.includes('\\') ? '\\' : '/';
205
+ return separator;
206
+ }
194
207
  parts.pop();
195
- return '/' + parts.join('/');
208
+ const separator = expanded.includes('\\') ? '\\' : '/';
209
+ return separator + parts.join(separator);
196
210
  }
197
211
 
198
212
  renderBreadcrumb(dirPath) {
199
213
  if (!this.folderBrowser.breadcrumbEl) return;
200
214
 
201
215
  const expanded = dirPath === '~' ? this.folderBrowser.homePath : dirPath;
202
- const parts = expanded.split('/').filter(Boolean);
216
+ const parts = pathSplit(expanded);
217
+ const separator = expanded.includes('\\') ? '\\' : '/';
203
218
 
204
219
  let html = '';
205
- html += '<span class="folder-breadcrumb-segment" data-path="/">/ </span>';
220
+ html += `<span class="folder-breadcrumb-segment" data-path="${separator}">${separator} </span>`;
206
221
 
207
222
  let accumulated = '';
208
223
  for (let i = 0; i < parts.length; i++) {
209
- accumulated += '/' + parts[i];
224
+ accumulated += separator + parts[i];
210
225
  const isLast = i === parts.length - 1;
211
- html += '<span class="folder-breadcrumb-separator">/</span>';
226
+ html += `<span class="folder-breadcrumb-separator">${separator}</span>`;
212
227
  html += `<span class="folder-breadcrumb-segment${isLast ? '' : ''}" data-path="${this.escapeHtml(accumulated)}">${this.escapeHtml(parts[i])}</span>`;
213
228
  }
214
229
 
@@ -227,7 +242,7 @@ class ConversationManager {
227
242
  const expanded = currentPath === '~' ? this.folderBrowser.homePath : currentPath;
228
243
  this.closeFolderBrowser();
229
244
 
230
- const dirName = expanded.split('/').filter(Boolean).pop() || 'root';
245
+ const dirName = pathBasename(expanded) || 'root';
231
246
  window.dispatchEvent(new CustomEvent('create-new-conversation', {
232
247
  detail: { workingDirectory: expanded, title: dirName }
233
248
  }));
@@ -398,7 +413,7 @@ class ConversationManager {
398
413
  const timestamp = conv.created_at ? new Date(conv.created_at).toLocaleDateString() : 'Unknown';
399
414
  const agent = this.getAgentDisplayName(conv.agentType);
400
415
  const modelLabel = conv.model ? ` (${conv.model})` : '';
401
- const wd = conv.workingDirectory ? conv.workingDirectory.split('/').pop() : '';
416
+ const wd = conv.workingDirectory ? pathBasename(conv.workingDirectory) : '';
402
417
  const metaParts = [agent + modelLabel, timestamp];
403
418
  if (wd) metaParts.push(wd);
404
419
 
@@ -4,6 +4,15 @@
4
4
  * for Claude Code streaming execution display
5
5
  */
6
6
 
7
+ function pathSplit(p) {
8
+ return p.split(/[\/\\]/).filter(Boolean);
9
+ }
10
+
11
+ function pathBasename(p) {
12
+ const parts = pathSplit(p);
13
+ return parts.length ? parts.pop() : '';
14
+ }
15
+
7
16
  class StreamingRenderer {
8
17
  constructor(config = {}) {
9
18
  // Configuration
@@ -523,7 +532,7 @@ class StreamingRenderer {
523
532
  */
524
533
  renderFilePath(filePath) {
525
534
  if (!filePath) return '';
526
- const parts = filePath.split('/');
535
+ const parts = pathSplit(filePath);
527
536
  const fileName = parts.pop();
528
537
  const dir = parts.join('/');
529
538
  return `<div class="tool-param-file"><span class="file-icon">&#128196;</span>${dir ? `<span class="file-dir">${this.escapeHtml(dir)}/</span>` : ''}<span class="file-name">${this.escapeHtml(fileName)}</span></div>`;
@@ -660,18 +669,16 @@ class StreamingRenderer {
660
669
  getToolUseTitle(toolName, input) {
661
670
  const normalizedName = toolName.replace(/^mcp__[^_]+__/, '');
662
671
  if (normalizedName === 'Edit' && input.file_path) {
663
- const parts = input.file_path.split('/');
672
+ const parts = pathSplit(input.file_path);
664
673
  const fileName = parts.pop();
665
674
  const dir = parts.slice(-2).join('/');
666
675
  return dir ? `${dir}/${fileName}` : fileName;
667
676
  }
668
677
  if (normalizedName === 'Read' && input.file_path) {
669
- const parts = input.file_path.split('/');
670
- return parts.pop();
678
+ return pathBasename(input.file_path);
671
679
  }
672
680
  if (normalizedName === 'Write' && input.file_path) {
673
- const parts = input.file_path.split('/');
674
- return parts.pop();
681
+ return pathBasename(input.file_path);
675
682
  }
676
683
  if (normalizedName === 'Bash' || normalizedName === 'bash') {
677
684
  const cmd = input.command || input.commands || '';
@@ -684,7 +691,7 @@ class StreamingRenderer {
684
691
  try { return new URL(input.url).hostname; } catch (e) { return input.url.substring(0, 40); }
685
692
  }
686
693
  if (normalizedName === 'WebSearch' && input.query) return input.query.substring(0, 50);
687
- if (input.file_path) return input.file_path.split('/').pop();
694
+ if (input.file_path) return pathBasename(input.file_path);
688
695
  if (input.command) {
689
696
  const c = typeof input.command === 'string' ? input.command : JSON.stringify(input.command);
690
697
  return c.length > 50 ? c.substring(0, 47) + '...' : c;
@@ -748,11 +755,14 @@ class StreamingRenderer {
748
755
  }
749
756
 
750
757
  const lines = trimmed.split('\n');
751
- const allFilePaths = lines.length > 1 && lines.every(l => l.trim() === '' || l.trim().startsWith('/'));
758
+ const allFilePaths = lines.length > 1 && lines.every(l => {
759
+ const t = l.trim();
760
+ return t === '' || t.startsWith('/') || /^[A-Za-z]:[\\\/]/.test(t);
761
+ });
752
762
  if (allFilePaths && lines.filter(l => l.trim()).length > 0) {
753
763
  const fileHtml = lines.filter(l => l.trim()).map(l => {
754
764
  const p = l.trim();
755
- const parts = p.split('/');
765
+ const parts = pathSplit(p);
756
766
  const name = parts.pop();
757
767
  const dir = parts.join('/');
758
768
  return `<div style="display:flex;align-items:center;gap:0.375rem;padding:0.1875rem 0;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem"><span style="opacity:0.5">&#128196;</span><span style="color:var(--color-text-secondary)">${this.escapeHtml(dir)}/</span><span style="font-weight:600">${this.escapeHtml(name)}</span></div>`;
@@ -785,7 +795,8 @@ class StreamingRenderer {
785
795
  const lines = data.split('\n').length;
786
796
  return `<div style="font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;white-space:pre-wrap;word-break:break-all;max-height:200px;overflow-y:auto;background:var(--color-bg-code);color:#d1d5db;padding:0.5rem;border-radius:0.375rem;line-height:1.5">${this.escapeHtml(data.substring(0, 1000))}${data.length > 1000 ? '\n... (' + (data.length - 1000) + ' more chars, ' + lines + ' lines)' : ''}</div>`;
787
797
  }
788
- if (data.startsWith('/') && !data.includes(' ') && data.includes('.')) return this.renderFilePath(data);
798
+ const looksLikePath = data.startsWith('/') || /^[A-Za-z]:[\\\/]/.test(data);
799
+ if (looksLikePath && !data.includes(' ') && data.includes('.')) return this.renderFilePath(data);
789
800
  return `<span style="color:var(--color-text-primary)">${this.escapeHtml(data)}</span>`;
790
801
  }
791
802
 
@@ -986,11 +997,14 @@ class StreamingRenderer {
986
997
  return html;
987
998
  }
988
999
 
989
- const allFilePaths = lines.length > 1 && lines.every(l => l.trim() === '' || l.trim().startsWith('/'));
1000
+ const allFilePaths = lines.length > 1 && lines.every(l => {
1001
+ const t = l.trim();
1002
+ return t === '' || t.startsWith('/') || /^[A-Za-z]:[\\\/]/.test(t);
1003
+ });
990
1004
  if (allFilePaths && lines.filter(l => l.trim()).length > 0) {
991
1005
  const fileHtml = lines.filter(l => l.trim()).map(l => {
992
1006
  const p = l.trim();
993
- const parts = p.split('/');
1007
+ const parts = pathSplit(p);
994
1008
  const name = parts.pop();
995
1009
  const dir = parts.join('/');
996
1010
  return `<div style="display:flex;align-items:center;gap:0.375rem;padding:0.1875rem 0;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem"><span style="opacity:0.5">&#128196;</span><span style="color:var(--color-text-secondary)">${esc(dir)}/</span><span style="font-weight:600">${esc(name)}</span></div>`;
@@ -1103,15 +1117,15 @@ class StreamingRenderer {
1103
1117
 
1104
1118
  static getToolTitle(toolName, input) {
1105
1119
  const n = toolName.replace(/^mcp__[^_]+__/, '');
1106
- if (n === 'Edit' && input.file_path) { const p = input.file_path.split('/'); const f = p.pop(); const d = p.slice(-2).join('/'); return d ? d+'/'+f : f; }
1107
- if (n === 'Read' && input.file_path) return input.file_path.split('/').pop();
1108
- if (n === 'Write' && input.file_path) return input.file_path.split('/').pop();
1120
+ if (n === 'Edit' && input.file_path) { const p = pathSplit(input.file_path); const f = p.pop(); const d = p.slice(-2).join('/'); return d ? d+'/'+f : f; }
1121
+ if (n === 'Read' && input.file_path) return pathBasename(input.file_path);
1122
+ if (n === 'Write' && input.file_path) return pathBasename(input.file_path);
1109
1123
  if ((n === 'Bash' || n === 'bash') && (input.command || input.commands)) { const c = typeof (input.command||input.commands) === 'string' ? (input.command||input.commands) : JSON.stringify(input.command||input.commands); return c.length > 60 ? c.substring(0,57)+'...' : c; }
1110
1124
  if (n === 'Glob' && input.pattern) return input.pattern;
1111
1125
  if (n === 'Grep' && input.pattern) return input.pattern;
1112
1126
  if (n === 'WebFetch' && input.url) { try { return new URL(input.url).hostname; } catch(e) { return input.url.substring(0,40); } }
1113
1127
  if (n === 'WebSearch' && input.query) return input.query.substring(0,50);
1114
- if (input.file_path) return input.file_path.split('/').pop();
1128
+ if (input.file_path) return pathBasename(input.file_path);
1115
1129
  if (input.command) { const c = typeof input.command === 'string' ? input.command : JSON.stringify(input.command); return c.length > 50 ? c.substring(0,47)+'...' : c; }
1116
1130
  if (input.query) return input.query.substring(0,50);
1117
1131
  return '';
@@ -1134,8 +1148,9 @@ class StreamingRenderer {
1134
1148
  if (data.length > 500) {
1135
1149
  return `<div style="font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.75rem;white-space:pre-wrap;word-break:break-all;max-height:200px;overflow-y:auto;background:var(--color-bg-code);color:#d1d5db;padding:0.5rem;border-radius:0.375rem;line-height:1.5">${esc(data.substring(0, 1000))}${data.length > 1000 ? '\n... (' + (data.length - 1000) + ' more chars)' : ''}</div>`;
1136
1150
  }
1137
- if (data.startsWith('/') && !data.includes(' ') && data.includes('.')) {
1138
- const parts = data.split('/');
1151
+ const looksLikePath = /^[A-Za-z]:[\\\/]/.test(data) || data.startsWith('/');
1152
+ if (looksLikePath && !data.includes(' ') && data.includes('.')) {
1153
+ const parts = pathSplit(data);
1139
1154
  const name = parts.pop();
1140
1155
  const dir = parts.join('/');
1141
1156
  return `<div style="display:flex;align-items:center;gap:0.375rem;font-family:'Monaco','Menlo','Ubuntu Mono',monospace;font-size:0.8rem"><span style="opacity:0.5">&#128196;</span><span style="color:var(--color-text-secondary)">${esc(dir)}/</span><span style="font-weight:600">${esc(name)}</span></div>`;
package/telemetry-id ADDED
@@ -0,0 +1 @@
1
+ 814cd4b3-ffd4-44c7-abbd-b2d9e757f0fc