agentgui 1.0.319 → 1.0.320

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/.prd CHANGED
@@ -44,14 +44,9 @@
44
44
 
45
45
  ### Wave 1 (COMPLETE - all items removed from work breakdown)
46
46
 
47
- ### Wave 2 (Depends on Wave 1)
48
- 4. Obtain Pinata API keys (if available) OR document blocking status
49
- - Blocked by: Wave 1 item 3 (dry-run test)
50
- - Blocks: Real IPFS publishing
51
-
52
- 5. Design server.js integration strategy
53
- - Blocked by: Wave 1 item 1 (implementation verification)
54
- - Blocks: Actual integration
47
+ ### Wave 2 (COMPLETE)
48
+ Pinata API keys: BLOCKED - not available, user needs to sign up at https://www.pinata.cloud/
49
+ ✓ Integration strategy designed - 150-200 line implementation, backward compatible, per-file iteration
55
50
 
56
51
  ### Wave 3 (Depends on Wave 2)
57
52
  6. Execute publish-models-to-ipfs.js to get real CIDs (if keys available)
package/database.js CHANGED
@@ -400,13 +400,13 @@ try {
400
400
  console.log('[MODELS] Registered Whisper STT IPFS CID:', WHISPER_CID);
401
401
  }
402
402
 
403
- const existingTTS = db.prepare('SELECT * FROM ipfs_cids WHERE modelName = ? AND modelType = ?').get('tts', 'voice');
403
+ const existingTTS = db.prepare('SELECT * FROM ipfs_cids WHERE modelName = ? AND modelType = ?').get('tts-models', 'voice');
404
404
  if (!existingTTS) {
405
405
  const cidId = `cid-${Date.now()}-tts`;
406
406
  db.prepare(
407
407
  `INSERT INTO ipfs_cids (id, cid, modelName, modelType, modelHash, gatewayUrl, cached_at, last_accessed_at)
408
408
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
409
- ).run(cidId, TTS_CID, 'tts', 'voice', 'sha256-verified', LIGHTHOUSE_GATEWAY, Date.now(), Date.now());
409
+ ).run(cidId, TTS_CID, 'tts-models', 'voice', 'sha256-verified', LIGHTHOUSE_GATEWAY, Date.now(), Date.now());
410
410
  console.log('[MODELS] Registered TTS IPFS CID:', TTS_CID);
411
411
  }
412
412
  } catch (err) {
@@ -0,0 +1,79 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+
5
+ const METRICS_PATH = path.join(os.homedir(), '.gmgui', 'models', '.metrics.json');
6
+
7
+ export function recordMetric(metric) {
8
+ const metricsDir = path.dirname(METRICS_PATH);
9
+ if (!fs.existsSync(metricsDir)) {
10
+ fs.mkdirSync(metricsDir, { recursive: true });
11
+ }
12
+
13
+ let metrics = [];
14
+ if (fs.existsSync(METRICS_PATH)) {
15
+ try {
16
+ metrics = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
17
+ } catch (e) {
18
+ metrics = [];
19
+ }
20
+ }
21
+
22
+ metrics.push({
23
+ ...metric,
24
+ timestamp: new Date().toISOString()
25
+ });
26
+
27
+ const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
28
+ metrics = metrics.filter(m => new Date(m.timestamp).getTime() > oneDayAgo);
29
+
30
+ fs.writeFileSync(METRICS_PATH, JSON.stringify(metrics, null, 2));
31
+ }
32
+
33
+ export function getMetrics() {
34
+ if (!fs.existsSync(METRICS_PATH)) {
35
+ return [];
36
+ }
37
+ return JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
38
+ }
39
+
40
+ export function getMetricsSummary() {
41
+ const metrics = getMetrics();
42
+
43
+ const summary = {
44
+ total: metrics.length,
45
+ cache_hits: metrics.filter(m => m.layer === 'cache' && m.status === 'hit').length,
46
+ ipfs: {
47
+ success: metrics.filter(m => m.layer === 'ipfs' && m.status === 'success').length,
48
+ error: metrics.filter(m => m.layer === 'ipfs' && m.status === 'error').length,
49
+ avg_latency: 0
50
+ },
51
+ huggingface: {
52
+ success: metrics.filter(m => m.layer === 'huggingface' && m.status === 'success').length,
53
+ error: metrics.filter(m => m.layer === 'huggingface' && m.status === 'error').length,
54
+ avg_latency: 0
55
+ }
56
+ };
57
+
58
+ const ipfsSuccess = metrics.filter(m => m.layer === 'ipfs' && m.status === 'success');
59
+ if (ipfsSuccess.length > 0) {
60
+ summary.ipfs.avg_latency = Math.round(
61
+ ipfsSuccess.reduce((sum, m) => sum + m.latency_ms, 0) / ipfsSuccess.length
62
+ );
63
+ }
64
+
65
+ const hfSuccess = metrics.filter(m => m.layer === 'huggingface' && m.status === 'success');
66
+ if (hfSuccess.length > 0) {
67
+ summary.huggingface.avg_latency = Math.round(
68
+ hfSuccess.reduce((sum, m) => sum + m.latency_ms, 0) / hfSuccess.length
69
+ );
70
+ }
71
+
72
+ return summary;
73
+ }
74
+
75
+ export function resetMetrics() {
76
+ if (fs.existsSync(METRICS_PATH)) {
77
+ fs.unlinkSync(METRICS_PATH);
78
+ }
79
+ }
@@ -0,0 +1,26 @@
1
+ import fs from 'fs';
2
+ import crypto from 'crypto';
3
+
4
+ export function verifyFileIntegrity(filepath, expectedHash, minBytes) {
5
+ if (!fs.existsSync(filepath)) {
6
+ return { valid: false, reason: 'file_not_found' };
7
+ }
8
+
9
+ const stats = fs.statSync(filepath);
10
+ if (minBytes && stats.size < minBytes) {
11
+ return { valid: false, reason: 'size_too_small', actual: stats.size, expected: minBytes };
12
+ }
13
+
14
+ if (expectedHash) {
15
+ const hash = crypto.createHash('sha256');
16
+ const data = fs.readFileSync(filepath);
17
+ hash.update(data);
18
+ const actualHash = hash.digest('hex');
19
+
20
+ if (actualHash !== expectedHash) {
21
+ return { valid: false, reason: 'hash_mismatch', actual: actualHash, expected: expectedHash };
22
+ }
23
+ }
24
+
25
+ return { valid: true };
26
+ }
@@ -1,8 +1,8 @@
1
1
  import { createRequire } from 'module';
2
2
  import fs from 'fs';
3
3
  import path from 'path';
4
- import crypto from 'crypto';
5
- import os from 'os';
4
+ import { recordMetric } from './download-metrics.js';
5
+ import { verifyFileIntegrity } from './file-verification.js';
6
6
 
7
7
  const require = createRequire(import.meta.url);
8
8
 
@@ -13,58 +13,6 @@ const GATEWAYS = [
13
13
  'https://ipfs.io/ipfs/'
14
14
  ];
15
15
 
16
- const METRICS_PATH = path.join(os.homedir(), '.gmgui', 'models', '.metrics.json');
17
-
18
- function recordMetric(metric) {
19
- const metricsDir = path.dirname(METRICS_PATH);
20
- if (!fs.existsSync(metricsDir)) {
21
- fs.mkdirSync(metricsDir, { recursive: true });
22
- }
23
-
24
- let metrics = [];
25
- if (fs.existsSync(METRICS_PATH)) {
26
- try {
27
- metrics = JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
28
- } catch (e) {
29
- metrics = [];
30
- }
31
- }
32
-
33
- metrics.push({
34
- ...metric,
35
- timestamp: new Date().toISOString()
36
- });
37
-
38
- const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
39
- metrics = metrics.filter(m => new Date(m.timestamp).getTime() > oneDayAgo);
40
-
41
- fs.writeFileSync(METRICS_PATH, JSON.stringify(metrics, null, 2));
42
- }
43
-
44
- function verifyFileIntegrity(filepath, expectedHash, minBytes) {
45
- if (!fs.existsSync(filepath)) {
46
- return { valid: false, reason: 'file_not_found' };
47
- }
48
-
49
- const stats = fs.statSync(filepath);
50
- if (minBytes && stats.size < minBytes) {
51
- return { valid: false, reason: 'size_too_small', actual: stats.size, expected: minBytes };
52
- }
53
-
54
- if (expectedHash) {
55
- const hash = crypto.createHash('sha256');
56
- const data = fs.readFileSync(filepath);
57
- hash.update(data);
58
- const actualHash = hash.digest('hex');
59
-
60
- if (actualHash !== expectedHash) {
61
- return { valid: false, reason: 'hash_mismatch', actual: actualHash, expected: expectedHash };
62
- }
63
- }
64
-
65
- return { valid: true };
66
- }
67
-
68
16
  async function downloadFromIPFS(cid, destPath, manifest, onProgress) {
69
17
  const startTime = Date.now();
70
18
 
@@ -239,50 +187,4 @@ export async function downloadWithFallback(options, onProgress) {
239
187
  throw new Error('All download layers exhausted');
240
188
  }
241
189
 
242
- export function getMetrics() {
243
- if (!fs.existsSync(METRICS_PATH)) {
244
- return [];
245
- }
246
- return JSON.parse(fs.readFileSync(METRICS_PATH, 'utf8'));
247
- }
248
-
249
- export function getMetricsSummary() {
250
- const metrics = getMetrics();
251
-
252
- const summary = {
253
- total: metrics.length,
254
- cache_hits: metrics.filter(m => m.layer === 'cache' && m.status === 'hit').length,
255
- ipfs: {
256
- success: metrics.filter(m => m.layer === 'ipfs' && m.status === 'success').length,
257
- error: metrics.filter(m => m.layer === 'ipfs' && m.status === 'error').length,
258
- avg_latency: 0
259
- },
260
- huggingface: {
261
- success: metrics.filter(m => m.layer === 'huggingface' && m.status === 'success').length,
262
- error: metrics.filter(m => m.layer === 'huggingface' && m.status === 'error').length,
263
- avg_latency: 0
264
- }
265
- };
266
-
267
- const ipfsSuccess = metrics.filter(m => m.layer === 'ipfs' && m.status === 'success');
268
- if (ipfsSuccess.length > 0) {
269
- summary.ipfs.avg_latency = Math.round(
270
- ipfsSuccess.reduce((sum, m) => sum + m.latency_ms, 0) / ipfsSuccess.length
271
- );
272
- }
273
-
274
- const hfSuccess = metrics.filter(m => m.layer === 'huggingface' && m.status === 'success');
275
- if (hfSuccess.length > 0) {
276
- summary.huggingface.avg_latency = Math.round(
277
- hfSuccess.reduce((sum, m) => sum + m.latency_ms, 0) / hfSuccess.length
278
- );
279
- }
280
-
281
- return summary;
282
- }
283
-
284
- export function resetMetrics() {
285
- if (fs.existsSync(METRICS_PATH)) {
286
- fs.unlinkSync(METRICS_PATH);
287
- }
288
- }
190
+ export { getMetrics, getMetricsSummary, resetMetrics } from './download-metrics.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.319",
3
+ "version": "1.0.320",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -14,6 +14,7 @@ import Busboy from 'busboy';
14
14
  import fsbrowse from 'fsbrowse';
15
15
  import { queries } from './database.js';
16
16
  import { runClaudeWithStreaming } from './lib/claude-runner.js';
17
+ import { downloadWithFallback } from './lib/model-downloader.js';
17
18
  const { downloadWithProgress } = createRequire(import.meta.url)('webtalk/ipfs-downloader');
18
19
 
19
20
  const ttsTextAccumulators = new Map();
@@ -77,39 +78,70 @@ async function ensureModelsDownloaded() {
77
78
  ? (fs.existsSync(path.join(process.env.PORTABLE_EXE_DIR, 'models', 'onnx-community')) ? path.join(process.env.PORTABLE_EXE_DIR, 'models') : gmguiModels)
78
79
  : gmguiModels;
79
80
 
80
- const { ensureModels } = createRequire(import.meta.url)('webtalk/ipfs-downloader');
81
- const { createConfig } = createRequire(import.meta.url)('webtalk/config');
82
- const config = createConfig({
83
- modelsDir: modelsBase,
84
- ttsModelsDir: path.join(modelsBase, 'tts'),
85
- sttModelsDir: path.join(modelsBase, 'stt'),
86
- });
81
+ const manifestPath = path.join(gmguiModels, '.manifests.json');
82
+ if (!fs.existsSync(manifestPath)) {
83
+ throw new Error('Model manifest not found at ' + manifestPath);
84
+ }
87
85
 
88
- const { checkTTSModelExists } = createRequire(import.meta.url)('webtalk/tts-models');
89
- const { checkWhisperModelExists } = createRequire(import.meta.url)('webtalk/whisper-models');
86
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
90
87
 
91
- const sttOk = await checkWhisperModelExists(config.defaultWhisperModel, config).catch(() => false);
92
- const ttsOk = await checkTTSModelExists(config).catch(() => false);
93
-
94
- if (sttOk && ttsOk) {
95
- console.log('[MODELS] All model files present');
96
- modelDownloadState.complete = true;
97
- return true;
98
- }
88
+ const whisperCidRecord = queries.getIpfsCidByModel('whisper-base', 'stt');
89
+ const ttsCidRecord = queries.getIpfsCidByModel('tts', 'voice');
99
90
 
100
91
  modelDownloadState.downloading = true;
101
92
  modelDownloadState.error = null;
102
93
 
103
- await ensureModels(config, (progress) => {
104
- broadcastModelProgress({
105
- started: true,
106
- done: progress.done || false,
107
- downloading: progress.status === 'downloading',
108
- type: progress.type,
109
- source: 'ipfs',
110
- ...progress,
111
- });
112
- });
94
+ const downloadModel = async (modelName, modelType, cidRecord) => {
95
+ const modelManifest = manifest[modelName];
96
+ if (!modelManifest) {
97
+ throw new Error(`Model ${modelName} not found in manifest`);
98
+ }
99
+
100
+ const isWhisper = modelType === 'stt';
101
+ const baseDir = isWhisper
102
+ ? path.join(modelsBase, 'onnx-community', 'whisper-base')
103
+ : path.join(modelsBase, 'tts');
104
+
105
+ fs.mkdirSync(baseDir, { recursive: true });
106
+
107
+ for (const [filename, fileInfo] of Object.entries(modelManifest.files)) {
108
+ const destPath = path.join(baseDir, filename);
109
+
110
+ if (fs.existsSync(destPath) && fs.statSync(destPath).size === fileInfo.size) {
111
+ console.log(`[MODELS] ${filename} already exists, skipping`);
112
+ continue;
113
+ }
114
+
115
+ const ipfsCid = cidRecord ? `${cidRecord.cid}/${filename}` : null;
116
+ const huggingfaceUrl = isWhisper
117
+ ? `https://huggingface.co/onnx-community/whisper-base/resolve/main/${filename}`
118
+ : `https://huggingface.co/datasets/AnEntrypoint/sttttsmodels/resolve/main/tts/${filename}`;
119
+
120
+ await downloadWithFallback({
121
+ ipfsCid,
122
+ huggingfaceUrl,
123
+ destPath,
124
+ manifest: fileInfo,
125
+ minBytes: fileInfo.size * 0.8,
126
+ preferredLayer: ipfsCid ? 'ipfs' : 'huggingface'
127
+ }, (progress) => {
128
+ broadcastModelProgress({
129
+ started: true,
130
+ done: progress.status === 'success',
131
+ downloading: progress.status === 'downloading',
132
+ type: modelType === 'stt' ? 'whisper' : 'tts',
133
+ source: progress.layer === 'cache' ? 'cache' : progress.layer,
134
+ status: progress.status,
135
+ file: filename,
136
+ progress: progress.total ? (progress.downloaded / progress.total * 100) : 0,
137
+ gateway: progress.gateway
138
+ });
139
+ });
140
+ }
141
+ };
142
+
143
+ await downloadModel('whisper-base', 'stt', whisperCidRecord);
144
+ await downloadModel('tts-models', 'voice', ttsCidRecord);
113
145
 
114
146
  modelDownloadState.complete = true;
115
147
  broadcastModelProgress({ started: true, done: true, downloading: false });
package/static/index.html CHANGED
@@ -727,6 +727,26 @@
727
727
  ::-webkit-scrollbar-thumb { background-color: var(--color-border); border-radius: 3px; }
728
728
  ::-webkit-scrollbar-thumb:hover { background-color: var(--color-text-tertiary); }
729
729
 
730
+ /* DIALOGS */
731
+ .dialog-overlay { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; }
732
+ .dialog-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.5); }
733
+ .dialog-box { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); background: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: 8px; padding: 24px; min-width: 320px; max-width: 480px; box-shadow: 0 8px 32px rgba(0,0,0,0.2); }
734
+ .dialog-box h3, .dialog-box .dialog-title { font-size: 1rem; font-weight: 600; margin-bottom: 12px; color: var(--color-text-primary); }
735
+ .dialog-box p, .dialog-box .dialog-message { color: var(--color-text-secondary); margin-bottom: 16px; font-size: 0.875rem; }
736
+ .dialog-box input, .dialog-box textarea { width: 100%; padding: 8px 12px; border: 1px solid var(--color-border); border-radius: 6px; background: var(--color-bg-tertiary); color: var(--color-text-primary); font-size: 0.875rem; margin-bottom: 16px; }
737
+ .dialog-footer { display: flex; gap: 8px; justify-content: flex-end; }
738
+ .dialog-btn { padding: 6px 16px; border-radius: 6px; border: 1px solid var(--color-border); background: var(--color-bg-tertiary); color: var(--color-text-primary); cursor: pointer; font-size: 0.875rem; }
739
+ .dialog-btn-primary { background: var(--color-primary); border-color: var(--color-primary); color: #fff; }
740
+ .dialog-btn:hover { opacity: 0.85; }
741
+ .dialog-box-progress .dialog-progress-bar { height: 4px; background: var(--color-border); border-radius: 2px; overflow: hidden; margin-bottom: 8px; }
742
+ .dialog-box-progress .dialog-progress-fill { height: 100%; background: var(--color-primary); transition: width 0.3s; }
743
+
744
+ /* TOOL ICONS */
745
+ .folded-tool-icon { display: inline-flex; align-items: center; flex-shrink: 0; }
746
+ .folded-tool-icon svg { width: 16px; height: 16px; flex-shrink: 0; }
747
+ .folded-tool-bar { display: flex; align-items: center; gap: 6px; }
748
+ .tool-result-status { display: flex; align-items: center; gap: 6px; }
749
+
730
750
  /* RESPONSIVE */
731
751
  @media (max-width: 768px) {
732
752
  .sidebar {