agentgui 1.0.130 → 1.0.138

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/CLAUDE.md CHANGED
@@ -128,6 +128,51 @@ Production ready - no additional configuration needed beyond:
128
128
  2. Set PORT environment variable if needed
129
129
  3. Run: `npm start` or `npm run dev` for development
130
130
 
131
+ ## npm Publishing Setup
132
+
133
+ Automated npm publishing is configured via GitHub Actions with OIDC authentication. To complete setup:
134
+
135
+ ### Step 1: Configure OIDC Trusted Publisher on npm.org
136
+
137
+ Visit: https://www.npmjs.com/package/agentgui/access
138
+
139
+ Click "Add Trusted Publisher" and fill in:
140
+ - **Publishing provider**: GitHub
141
+ - **Owner**: AnEntrypoint
142
+ - **Repository**: agentgui
143
+ - **Workflow file**: `.github/workflows/publish-npm.yml`
144
+
145
+ This requires npm account access with 2FA completion.
146
+
147
+ ### Step 2: Trigger Publishing Workflow
148
+
149
+ Once OIDC is configured, push to main branch to trigger automatic publishing:
150
+
151
+ ```bash
152
+ git commit --allow-empty -m "test: verify npm publish with OIDC"
153
+ git push
154
+ ```
155
+
156
+ Monitor at: https://github.com/AnEntrypoint/agentgui/actions
157
+
158
+ ### Optional: Add Granular Token Backup
159
+
160
+ Generate a 3-month granular access token for fallback authentication:
161
+
162
+ Visit: https://www.npmjs.com/settings/lanmower/tokens
163
+
164
+ Click "Generate New Token" → "Granular Access Token" and configure:
165
+ - **Name**: github-actions-3month
166
+ - **Permissions**: Read and write
167
+ - **Package**: agentgui
168
+ - **Expiration**: 90 days
169
+ - **Bypass 2FA**: enabled
170
+
171
+ Then add to GitHub Actions secrets:
172
+ ```bash
173
+ gh secret set NPM_TOKEN --body "YOUR_TOKEN" --repo AnEntrypoint/agentgui
174
+ ```
175
+
131
176
  ## Support
132
177
 
133
178
  For issues, check:
@@ -135,3 +180,4 @@ For issues, check:
135
180
  - Server logs for backend issues
136
181
  - Database at `./data/agentgui.db` for data persistence
137
182
  - WebSocket connection in Network tab (should show `/sync` as connected)
183
+ - GitHub Actions: https://github.com/AnEntrypoint/agentgui/actions for publishing errors
package/bin/gmgui.cjs CHANGED
@@ -44,7 +44,7 @@ async function gmgui(args = []) {
44
44
  return new Promise((resolve, reject) => {
45
45
  const ps = spawn(runtime, [path.join(projectRoot, 'server.js')], {
46
46
  cwd: projectRoot,
47
- env: { ...process.env, PORT: port, BASE_URL: baseUrl },
47
+ env: { ...process.env, PORT: port, BASE_URL: baseUrl, STARTUP_CWD: process.cwd() },
48
48
  stdio: 'inherit'
49
49
  });
50
50
 
package/database.js CHANGED
@@ -280,7 +280,7 @@ export const queries = {
280
280
 
281
281
  getConversationsList() {
282
282
  const stmt = db.prepare(
283
- 'SELECT id, title, agentType, created_at, updated_at, messageCount, workingDirectory FROM conversations WHERE status != ? ORDER BY updated_at DESC'
283
+ 'SELECT id, title, agentType, created_at, updated_at, messageCount, workingDirectory, isStreaming FROM conversations WHERE status != ? ORDER BY updated_at DESC'
284
284
  );
285
285
  return stmt.all('deleted');
286
286
  },
@@ -573,11 +573,19 @@ export const queries = {
573
573
  }
574
574
 
575
575
  const deleteStmt = db.transaction(() => {
576
+ const sessionIds = db.prepare('SELECT id FROM sessions WHERE conversationId = ?').all(id).map(r => r.id);
577
+ db.prepare('DELETE FROM stream_updates WHERE conversationId = ?').run(id);
576
578
  db.prepare('DELETE FROM chunks WHERE conversationId = ?').run(id);
577
579
  db.prepare('DELETE FROM events WHERE conversationId = ?').run(id);
580
+ if (sessionIds.length > 0) {
581
+ const placeholders = sessionIds.map(() => '?').join(',');
582
+ db.prepare(`DELETE FROM stream_updates WHERE sessionId IN (${placeholders})`).run(...sessionIds);
583
+ db.prepare(`DELETE FROM chunks WHERE sessionId IN (${placeholders})`).run(...sessionIds);
584
+ db.prepare(`DELETE FROM events WHERE sessionId IN (${placeholders})`).run(...sessionIds);
585
+ }
578
586
  db.prepare('DELETE FROM sessions WHERE conversationId = ?').run(id);
579
587
  db.prepare('DELETE FROM messages WHERE conversationId = ?').run(id);
580
- db.prepare('UPDATE conversations SET status = ? WHERE id = ?').run('deleted', id);
588
+ db.prepare('DELETE FROM conversations WHERE id = ?').run(id);
581
589
  });
582
590
 
583
591
  deleteStmt();
@@ -1064,6 +1072,7 @@ export const queries = {
1064
1072
  }
1065
1073
 
1066
1074
  const deleteStmt = db.transaction(() => {
1075
+ db.prepare('DELETE FROM stream_updates WHERE conversationId = ?').run(id);
1067
1076
  db.prepare('DELETE FROM chunks WHERE conversationId = ?').run(id);
1068
1077
  db.prepare('DELETE FROM events WHERE conversationId = ?').run(id);
1069
1078
  db.prepare('DELETE FROM sessions WHERE conversationId = ?').run(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.130",
3
+ "version": "1.0.138",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -26,6 +26,7 @@
26
26
  "busboy": "^1.6.0",
27
27
  "express": "^5.2.1",
28
28
  "fsbrowse": "^0.2.13",
29
+ "webtalk": "github:anEntrypoint/realtime-whisper-webgpu",
29
30
  "ws": "^8.14.2"
30
31
  }
31
32
  }
package/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import http from 'http';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
+ import os from 'os';
4
5
  import { fileURLToPath } from 'url';
5
6
  import { WebSocketServer } from 'ws';
6
7
  import { execSync } from 'child_process';
@@ -11,7 +12,8 @@ import { runClaudeWithStreaming } from './lib/claude-runner.js';
11
12
  const require = createRequire(import.meta.url);
12
13
  const express = require('express');
13
14
  const Busboy = require('busboy');
14
- const fsbrowse = require('../fsbrowse');
15
+ const fsbrowse = require('fsbrowse');
16
+ const { webtalk } = require('webtalk');
15
17
 
16
18
  const SYSTEM_PROMPT = `Always write your responses in ripple-ui enhanced HTML. Avoid overriding light/dark mode CSS variables. Use all the benefits of HTML to express technical details with proper semantic markup, tables, code blocks, headings, and lists. Write clean, well-structured HTML that respects the existing design system.`;
17
19
 
@@ -28,12 +30,18 @@ const PORT = process.env.PORT || 3000;
28
30
  const BASE_URL = (process.env.BASE_URL || '/gm').replace(/\/+$/, '');
29
31
  const watch = process.argv.includes('--no-watch') ? false : (process.argv.includes('--watch') || process.env.HOT_RELOAD !== 'false');
30
32
 
33
+ const STARTUP_CWD = process.env.STARTUP_CWD || process.cwd();
31
34
  const staticDir = path.join(__dirname, 'static');
32
35
  if (!fs.existsSync(staticDir)) fs.mkdirSync(staticDir, { recursive: true });
33
36
 
34
37
  // Express sub-app for fsbrowse file browser and file upload
35
38
  const expressApp = express();
36
39
 
40
+ // Separate Express app for webtalk (STT/TTS) - isolated to contain COEP/COOP headers
41
+ const webtalkApp = express();
42
+ const webtalkInstance = webtalk(webtalkApp, { path: '/webtalk' });
43
+ webtalkInstance.init().catch(err => debugLog('Webtalk init: ' + err.message));
44
+
37
45
  // File upload endpoint - copies dropped files to conversation workingDirectory
38
46
  expressApp.post(BASE_URL + '/api/upload/:conversationId', (req, res) => {
39
47
  try {
@@ -139,14 +147,72 @@ const server = http.createServer(async (req, res) => {
139
147
  res.setHeader('Access-Control-Allow-Origin', '*');
140
148
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
141
149
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
150
+ res.setHeader('Cross-Origin-Embedder-Policy', 'credentialless');
151
+ res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
152
+ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
142
153
  if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
143
154
 
144
- // Route file upload and fsbrowse requests through Express sub-app
145
155
  const pathOnly = req.url.split('?')[0];
156
+ const webtalkPrefix = BASE_URL + '/webtalk';
157
+ const isWebtalkRoute = pathOnly.startsWith(webtalkPrefix) ||
158
+ pathOnly.startsWith(BASE_URL + '/api/tts-status') ||
159
+ pathOnly.startsWith(BASE_URL + '/assets/') ||
160
+ pathOnly.startsWith(BASE_URL + '/tts/') ||
161
+ pathOnly.startsWith(BASE_URL + '/models/') ||
162
+ pathOnly.startsWith('/webtalk') ||
163
+ pathOnly.startsWith('/assets/') ||
164
+ pathOnly.startsWith('/tts/') ||
165
+ pathOnly.startsWith('/models/');
166
+ if (isWebtalkRoute) {
167
+ const webtalkSdkDir = path.dirname(require.resolve('webtalk/package.json'));
168
+ const sdkFiles = { '/demo': 'app.html', '/sdk.js': 'sdk.js', '/stt.js': 'stt.js', '/tts.js': 'tts.js', '/tts-utils.js': 'tts-utils.js' };
169
+ let stripped = pathOnly.startsWith(webtalkPrefix) ? pathOnly.slice(webtalkPrefix.length) : (pathOnly.startsWith('/webtalk') ? pathOnly.slice('/webtalk'.length) : null);
170
+ if (stripped !== null && !sdkFiles[stripped] && !stripped.endsWith('.js') && sdkFiles[stripped + '.js']) stripped += '.js';
171
+ if (stripped !== null && sdkFiles[stripped]) {
172
+ const filePath = path.join(webtalkSdkDir, sdkFiles[stripped]);
173
+ return fs.readFile(filePath, 'utf-8', (err, content) => {
174
+ if (err) { res.writeHead(404); res.end('Not found'); return; }
175
+ if (stripped === '/demo') {
176
+ let patched = content
177
+ .replace(/from\s+['"](\/webtalk\/[^'"]+)['"]/g, (_, p) => `from '${BASE_URL}${p}'`)
178
+ .replace(/from\s+['"]\.\/([^'"]+)['"]/g, (_, p) => `from '${BASE_URL}/webtalk/${p}'`)
179
+ .replace('<head>', `<head>\n <script>window.__WEBTALK_BASE='${BASE_URL}';</script>`);
180
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cross-Origin-Embedder-Policy': 'credentialless', 'Cross-Origin-Opener-Policy': 'same-origin', 'Cross-Origin-Resource-Policy': 'cross-origin' });
181
+ return res.end(patched);
182
+ }
183
+ let js = content;
184
+ const ensureExt = (mod) => mod.endsWith('.js') ? mod : mod + '.js';
185
+ if (js.includes('require(') || js.includes('module.exports')) {
186
+ js = js.replace(/const\s*\{([^}]+)\}\s*=\s*require\(['"]\.\/([^'"]+)['"]\);?/g, (_, names, mod) => `import {${names}} from '${BASE_URL}/webtalk/${ensureExt(mod)}';`);
187
+ js = js.replace(/const\s+(\w+)\s*=\s*require\(['"]\.\/([^'"]+)['"]\);?/g, (_, name, mod) => `import ${name} from '${BASE_URL}/webtalk/${ensureExt(mod)}';`);
188
+ js = js.replace(/module\.exports\s*=\s*\{([^}]+)\};?/, (_, names) => `export {${names.trim().replace(/\s+/g, ' ')} };`);
189
+ }
190
+ js = js.replace(/from\s+['"]\.\/([^'"]+)['"]/g, (_, p) => `from '${BASE_URL}/webtalk/${ensureExt(p)}'`);
191
+ res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8', 'Cross-Origin-Resource-Policy': 'cross-origin' });
192
+ res.end(js);
193
+ });
194
+ }
195
+ if (req.url.startsWith(BASE_URL)) req.url = req.url.slice(BASE_URL.length) || '/';
196
+ const origSetHeader = res.setHeader.bind(res);
197
+ res.setHeader = (name, value) => {
198
+ if (name.toLowerCase() === 'cross-origin-embedder-policy') return;
199
+ origSetHeader(name, value);
200
+ };
201
+ return webtalkApp(req, res);
202
+ }
203
+
204
+ // Route file upload and fsbrowse requests through Express sub-app
146
205
  if (pathOnly.startsWith(BASE_URL + '/api/upload/') || pathOnly.startsWith(BASE_URL + '/files/')) {
147
206
  return expressApp(req, res);
148
207
  }
149
208
 
209
+ if (req.url === '/favicon.ico' || req.url === BASE_URL + '/favicon.ico') {
210
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="20" fill="#3b82f6"/><text x="50" y="68" font-size="50" font-family="sans-serif" font-weight="bold" fill="white" text-anchor="middle">G</text></svg>';
211
+ res.writeHead(200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' });
212
+ res.end(svg);
213
+ return;
214
+ }
215
+
150
216
  if (req.url === '/') { res.writeHead(302, { Location: BASE_URL + '/' }); res.end(); return; }
151
217
 
152
218
  if (!req.url.startsWith(BASE_URL + '/') && req.url !== BASE_URL) {
@@ -424,16 +490,16 @@ const server = http.createServer(async (req, res) => {
424
490
 
425
491
  if (routePath === '/api/home' && req.method === 'GET') {
426
492
  res.writeHead(200, { 'Content-Type': 'application/json' });
427
- res.end(JSON.stringify({ home: process.env.HOME || '/config' }));
493
+ res.end(JSON.stringify({ home: os.homedir(), cwd: STARTUP_CWD }));
428
494
  return;
429
495
  }
430
496
 
431
497
  if (routePath === '/api/folders' && req.method === 'POST') {
432
498
  const body = await parseBody(req);
433
- const folderPath = body.path || '/config';
499
+ const folderPath = body.path || STARTUP_CWD;
434
500
  try {
435
501
  const expandedPath = folderPath.startsWith('~') ?
436
- folderPath.replace('~', process.env.HOME || '/config') : folderPath;
502
+ folderPath.replace('~', os.homedir()) : folderPath;
437
503
  const entries = fs.readdirSync(expandedPath, { withFileTypes: true });
438
504
  const folders = entries
439
505
  .filter(e => e.isDirectory() && !e.name.startsWith('.'))
@@ -452,7 +518,7 @@ const server = http.createServer(async (req, res) => {
452
518
  const imagePath = routePath.slice('/api/image/'.length);
453
519
  const decodedPath = decodeURIComponent(imagePath);
454
520
  const expandedPath = decodedPath.startsWith('~') ?
455
- decodedPath.replace('~', process.env.HOME || '/config') : decodedPath;
521
+ decodedPath.replace('~', os.homedir()) : decodedPath;
456
522
  const normalizedPath = path.normalize(expandedPath);
457
523
  if (!normalizedPath.startsWith('/') || normalizedPath.includes('..')) {
458
524
  res.writeHead(403); res.end('Forbidden'); return;
@@ -510,7 +576,7 @@ function serveFile(filePath, res) {
510
576
  if (err) { res.writeHead(500); res.end('Server error'); return; }
511
577
  let content = data.toString();
512
578
  if (ext === '.html') {
513
- const baseTag = `<script>window.__BASE_URL='${BASE_URL}';</script>`;
579
+ const baseTag = `<script>window.__BASE_URL='${BASE_URL}';</script>\n <script type="importmap">{"imports":{"webtalk-sdk":"${BASE_URL}/webtalk/sdk.js"}}</script>`;
514
580
  content = content.replace('<head>', '<head>\n ' + baseTag);
515
581
  if (watch) {
516
582
  content += `\n<script>(function(){const ws=new WebSocket('ws://'+location.host+'${BASE_URL}/hot-reload');ws.onmessage=e=>{if(JSON.parse(e.data).type==='reload')location.reload()};})();</script>`;
@@ -550,12 +616,13 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
550
616
  const startTime = Date.now();
551
617
  activeExecutions.set(conversationId, true);
552
618
  queries.setIsStreaming(conversationId, true);
619
+ queries.updateSession(sessionId, { status: 'active' });
553
620
 
554
621
  try {
555
622
  debugLog(`[stream] Starting: conversationId=${conversationId}, sessionId=${sessionId}`);
556
623
 
557
624
  const conv = queries.getConversation(conversationId);
558
- const cwd = conv?.workingDirectory || '/config';
625
+ const cwd = conv?.workingDirectory || STARTUP_CWD;
559
626
  const resumeSessionId = conv?.claudeSessionId || null;
560
627
 
561
628
  let allBlocks = [];
@@ -761,7 +828,7 @@ async function processMessage(conversationId, messageId, content, agentId) {
761
828
  debugLog(`[processMessage] Starting: conversationId=${conversationId}, agentId=${agentId}`);
762
829
 
763
830
  const conv = queries.getConversation(conversationId);
764
- const cwd = conv?.workingDirectory || '/config';
831
+ const cwd = conv?.workingDirectory || STARTUP_CWD;
765
832
  const resumeSessionId = conv?.claudeSessionId || null;
766
833
 
767
834
  let contentStr = typeof content === 'object' ? JSON.stringify(content) : content;
@@ -886,7 +953,7 @@ function broadcastSync(event) {
886
953
  shouldSend = true;
887
954
  } else if (event.conversationId && ws.subscriptions?.has(`conv-${event.conversationId}`)) {
888
955
  shouldSend = true;
889
- } else if (event.type === 'message_created' || event.type === 'conversation_created' || event.type === 'conversations_updated' || event.type === 'conversation_deleted' || event.type === 'queue_status') {
956
+ } else if (event.type === 'message_created' || event.type === 'conversation_created' || event.type === 'conversations_updated' || event.type === 'conversation_deleted' || event.type === 'queue_status' || event.type === 'streaming_start' || event.type === 'streaming_complete' || event.type === 'streaming_error') {
890
957
  shouldSend = true;
891
958
  }
892
959
 
@@ -0,0 +1,68 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "=========================================="
5
+ echo "npm Granular Access Token Setup (3-Month)"
6
+ echo "=========================================="
7
+ echo ""
8
+ echo "STEP 1: Manual token generation on npm.org"
9
+ echo "=========================================="
10
+ echo ""
11
+ echo "Please follow these steps:"
12
+ echo ""
13
+ echo "1. Open your browser and go to:"
14
+ echo " https://www.npmjs.com/settings/lanmower/tokens"
15
+ echo ""
16
+ echo "2. Click 'Generate New Token'"
17
+ echo ""
18
+ echo "3. Select 'Granular Access Token'"
19
+ echo ""
20
+ echo "4. Fill in the form:"
21
+ echo " - Token name: github-actions-3month"
22
+ echo " - Description: GitHub Actions npm publishing (3 month validity)"
23
+ echo " - Permissions: Read and write"
24
+ echo " - Packages: agentgui"
25
+ echo " - Expiration: 90 days"
26
+ echo " - Bypass 2FA: CHECKED"
27
+ echo ""
28
+ echo "5. Click 'Generate'"
29
+ echo ""
30
+ echo "6. COPY the token value (shown only once!)"
31
+ echo ""
32
+ echo "=========================================="
33
+ echo "STEP 2: Add token to GitHub Actions secret"
34
+ echo "=========================================="
35
+ echo ""
36
+ read -p "Paste your npm token here: " NPM_TOKEN
37
+
38
+ if [ -z "$NPM_TOKEN" ]; then
39
+ echo "Error: Token cannot be empty"
40
+ exit 1
41
+ fi
42
+
43
+ echo ""
44
+ echo "Setting GitHub Actions secret..."
45
+
46
+ gh secret set NPM_TOKEN --body "$NPM_TOKEN" --repo AnEntrypoint/agentgui
47
+
48
+ echo ""
49
+ echo "Verifying secret was set..."
50
+ gh secret list --repo AnEntrypoint/agentgui | grep NPM_TOKEN
51
+
52
+ echo ""
53
+ echo "=========================================="
54
+ echo "STEP 3: Test the workflow"
55
+ echo "=========================================="
56
+ echo ""
57
+ echo "Creating test commit to trigger workflow..."
58
+
59
+ git -C /home/user/agentgui commit --allow-empty -m "test: verify npm publishing with 3-month token"
60
+ git -C /home/user/agentgui push
61
+
62
+ echo ""
63
+ echo "Monitor the workflow at:"
64
+ echo "https://github.com/AnEntrypoint/agentgui/actions"
65
+ echo ""
66
+ echo "=========================================="
67
+ echo "Setup Complete!"
68
+ echo "=========================================="
package/static/app.js CHANGED
@@ -494,7 +494,8 @@ class GMGUIApp {
494
494
  codeLines.push(lines[i]);
495
495
  i++;
496
496
  }
497
- html += `<div class="code-block" data-language="${this.escapeHtml(lang)}"><pre><code>${this.escapeHtml(codeLines.join('\n'))}</code></pre></div>`;
497
+ const clCount = codeLines.length;
498
+ html += `<details class="collapsible-code"><summary class="collapsible-code-summary">${this.escapeHtml(lang)} - ${clCount} line${clCount !== 1 ? 's' : ''}</summary><div class="code-block" data-language="${this.escapeHtml(lang)}"><pre><code>${this.escapeHtml(codeLines.join('\n'))}</code></pre></div></details>`;
498
499
  i++;
499
500
  continue;
500
501
  }
@@ -592,9 +593,10 @@ class GMGUIApp {
592
593
  <div class="html-content">${code}</div>
593
594
  </div>`;
594
595
  } else {
595
- return `<div class="code-block" data-language="${this.escapeHtml(language)}">
596
+ const lcCount = code.split('\n').length;
597
+ return `<details class="collapsible-code"><summary class="collapsible-code-summary">${this.escapeHtml(language)} - ${lcCount} line${lcCount !== 1 ? 's' : ''}</summary><div class="code-block" data-language="${this.escapeHtml(language)}">
596
598
  <pre><code>${this.escapeHtml(code)}</code></pre>
597
- </div>`;
599
+ </div></details>`;
598
600
  }
599
601
  }
600
602