json-object-editor 0.10.662 → 0.10.663

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,3 +1,9 @@
1
+ ## 0.10.663
2
+
3
+ - Plugin inventory debug page
4
+ - Added `_www/plugins-test.html`, a secured debug page that lists all active plugins, their async/top-level methods, and which apps use them, sourced from the `plugin-utils` plugin.
5
+ - This page is primarily intended for AI agents and developers to quickly verify that methods like `chatgpt.autofill` and `chatgpt.widgetStart` exist on a given instance and to inspect plugin wiring per app.
6
+
1
7
  ## 0.10.662
2
8
 
3
9
  - React Form Integration with JSON Definitions
package/_www/mcp-nav.js CHANGED
@@ -11,6 +11,7 @@
11
11
  '<a href="/matrix.html" target="matrix_win" rel="noopener">Matrix</a>',
12
12
  '<a href="/mcp-prompt.html" target="mcp_prompt_win" rel="noopener">MCP Prompt</a>',
13
13
  '<a href="/ai-widget-test.html" target="ai_widget_test_win" rel="noopener">AI Widget</a>',
14
+ '<a href="/plugins-test.html" target="plugins_test_win" rel="noopener">Plugins</a>',
14
15
  '<span style="margin-left:auto"></span>',
15
16
  '<a href="/privacy" target="privacy_win" rel="noopener">Privacy</a>',
16
17
  '<a href="/terms" target="terms_win" rel="noopener">Terms</a>',
@@ -0,0 +1,333 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>JOE Plugins — Inventory</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <style>
8
+ body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:20px}
9
+ h1{margin:8px 0}
10
+ .small{font-size:12px;color:#666}
11
+ table{width:100%;border-collapse:collapse;margin-top:10px}
12
+ th,td{border:1px solid #e1e4e8;padding:8px;text-align:left;font-size:14px}
13
+ th.sortable{cursor:pointer}
14
+ th.sorted-asc, th.sorted-desc{background:#eef}
15
+ th .sort-ind{font-size:12px;opacity:.6;margin-left:6px}
16
+ .chips{display:flex;gap:6px;flex-wrap:wrap}
17
+ .chip{display:inline-block;padding:2px 8px;border-radius:12px;background:#eef;border:1px solid #ccd;font-size:12px}
18
+ .chip.joe{background:#11bcd6;color:#fff;border-color:#0fa5bb}
19
+ .chip.app{background:#e0ecff;border-color:#b3c7ff}
20
+ .chip.async{background:#dcfce7;border-color:#16a34a}
21
+ .chip.protected{background:#fee2e2;border-color:#dc2626}
22
+ .badge{display:inline-block;padding:2px 6px;border-radius:10px;font-size:12px;background:#eef;border:1px solid #ccd}
23
+ .badge.warn{background:#f0c040;color:#000;border-color:#e5b93b}
24
+ .badge.ok{background:#0a7d00;color:#fff;border-color:#0a7d00}
25
+ .badge.info{background:#e5e7eb;color:#111827;border-color:#d1d5db}
26
+ .t-center{text-align:center}
27
+ th{background:#f6f8fa}
28
+ .status-ok{color:#0a7d00;font-size:22px}
29
+ .status-bad{color:#b00020;font-size:22px}
30
+ .row{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin:8px 0}
31
+ a[target]{text-decoration:none}
32
+ code{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px}
33
+ </style>
34
+ </head>
35
+ <body>
36
+ <div id="mcp-nav"></div>
37
+ <script src="/JsonObjectEditor/_www/mcp-nav.js"></script>
38
+
39
+ <h1>Plugin Inventory</h1>
40
+ <div class="small">
41
+ Overview of active JOE plugins and their async methods, with app usage.
42
+ Useful for verifying that methods like <code>chatgpt.autofill</code> and
43
+ <code>chatgpt.widgetStart</code> are present on this instance.
44
+ </div>
45
+
46
+ <div class="row" style="margin-top:12px;">
47
+ <label for="base" style="margin:0">Base URL</label>
48
+ <input id="base" value="" placeholder="http://localhost:2025" style="min-width:260px" />
49
+ <button id="refresh">Refresh</button>
50
+ <span id="status" class="small"></span>
51
+ </div>
52
+
53
+ <div class="row" style="margin-top:4px;">
54
+ <label for="appFilter" style="margin:0">Filter by App</label>
55
+ <select id="appFilter">
56
+ <option value="">All</option>
57
+ </select>
58
+ <label for="nameFilter" style="margin:0">Filter by Plugin</label>
59
+ <input id="nameFilter" placeholder="plugin name contains..." style="min-width:200px" />
60
+ </div>
61
+
62
+ <table>
63
+ <thead>
64
+ <tr>
65
+ <th class="sortable" data-key="name">Plugin <span class="sort-ind"></span></th>
66
+ <th>Path</th>
67
+ <th class="sortable" data-key="methodCount">Async Methods <span class="sort-ind"></span></th>
68
+ <th>Methods</th>
69
+ <th>Apps Using</th>
70
+ <th class="sortable" data-key="protectedCount">Protected <span class="sort-ind"></span></th>
71
+ <th>Notes</th>
72
+ </tr>
73
+ </thead>
74
+ <tbody id="rows"></tbody>
75
+ </table>
76
+
77
+ <script>
78
+ (function(){
79
+ var $ = function(id){ return document.getElementById(id); };
80
+ var base = $('base');
81
+ var refresh = $('refresh');
82
+ var status = $('status');
83
+ var rows = $('rows');
84
+ var appFilterSel = $('appFilter');
85
+ var nameFilter = $('nameFilter');
86
+ base.value = base.value || location.origin;
87
+
88
+ function setStatus(msg, ok){
89
+ status.textContent = msg||'';
90
+ status.className = 'small ' + (ok===true?'status-ok': ok===false?'status-bad':'');
91
+ }
92
+
93
+ async function fetchJSON(url, opts){
94
+ const res = await fetch(url, opts);
95
+ if(!res.ok){
96
+ let body;
97
+ try{ body = await res.text(); }catch(_e){ body=''; }
98
+ throw new Error('HTTP '+res.status+' '+body);
99
+ }
100
+ return res.json();
101
+ }
102
+
103
+ async function callMCP(baseUrl, method, params){
104
+ const url = baseUrl + '/mcp';
105
+ const body = { jsonrpc:'2.0', id:String(Date.now()), method:method, params:params||{} };
106
+ const resp = await fetch(url, {
107
+ method:'POST',
108
+ headers:{'Content-Type':'application/json'},
109
+ body: JSON.stringify(body)
110
+ });
111
+ if(!resp.ok){
112
+ throw new Error('HTTP '+resp.status+' '+(await resp.text()));
113
+ }
114
+ const j = await resp.json();
115
+ return (j && (j.result || j)) || {};
116
+ }
117
+
118
+ async function load(){
119
+ try{
120
+ setStatus('Loading...', null);
121
+ rows.innerHTML = '';
122
+ var baseUrl = base.value.replace(/\/$/,'');
123
+
124
+ // 1) Load plugin list via plugin-utils
125
+ var plugData = await fetchJSON(baseUrl + '/API/plugin/plugin-utils');
126
+ var plugins = (plugData && plugData.plugins) || {};
127
+
128
+ // 2) Load apps via MCP listApps so we can see which apps reference which plugins
129
+ var appMap = {};
130
+ try{
131
+ var la = await callMCP(baseUrl, 'listApps', {});
132
+ appMap = (la && la.apps) || {};
133
+ }catch(_e){ appMap = {}; }
134
+
135
+ // Build app -> plugins map and plugin -> apps usage map
136
+ var pluginApps = {}; // { pluginName: [appName,...] }
137
+ Object.keys(appMap || {}).forEach(function(appName){
138
+ var app = appMap[appName] || {};
139
+ var appPlugins = Array.isArray(app.plugins) ? app.plugins : [];
140
+ appPlugins.forEach(function(p){
141
+ if(!p) return;
142
+ pluginApps[p] = pluginApps[p] || [];
143
+ if (pluginApps[p].indexOf(appName) === -1){
144
+ pluginApps[p].push(appName);
145
+ }
146
+ });
147
+ });
148
+
149
+ // Populate app filter options
150
+ if (appFilterSel){
151
+ // Clear existing (keep "All")
152
+ while(appFilterSel.options.length > 1){
153
+ appFilterSel.remove(1);
154
+ }
155
+ Object.keys(appMap || {}).sort().forEach(function(a){
156
+ var opt=document.createElement('option');
157
+ opt.value=a;
158
+ opt.textContent=a;
159
+ appFilterSel.appendChild(opt);
160
+ });
161
+ }
162
+
163
+ // Normalize plugin rows
164
+ var data = Object.keys(plugins || {}).sort().map(function(name){
165
+ var p = plugins[name] || {};
166
+ var asyncMethods = Array.isArray(p.async) ? p.async.slice().sort() : [];
167
+ var topLevelMethods = Array.isArray(p.methods) ? p.methods.slice().sort() : [];
168
+ var usedBy = (pluginApps[name] || []).slice().sort();
169
+ var protectedList = Array.isArray(p.protected) ? p.protected : [];
170
+ // Union of async/top-level/protected so we show everything.
171
+ var methodSet = {};
172
+ asyncMethods.forEach(function(m){ if(m){ methodSet[m] = true; }});
173
+ topLevelMethods.forEach(function(m){ if(m){ methodSet[m] = true; }});
174
+ protectedList.forEach(function(m){ if(m){ methodSet[m] = true; }});
175
+ var allMethods = Object.keys(methodSet).sort();
176
+ return {
177
+ name: name,
178
+ path: p._pathname || '',
179
+ override: !!p._override,
180
+ methodsAll: allMethods,
181
+ asyncMethods: asyncMethods,
182
+ methodCount: asyncMethods.length,
183
+ usedBy: usedBy,
184
+ protected: protectedList,
185
+ protectedCount: protectedList.length
186
+ };
187
+ });
188
+
189
+ function getFilteredData(){
190
+ var appSel = (appFilterSel && appFilterSel.value) || '';
191
+ var nameSel = (nameFilter && nameFilter.value || '').toLowerCase().trim();
192
+ return data.filter(function(d){
193
+ if (appSel && (!d.usedBy || d.usedBy.indexOf(appSel) === -1)){
194
+ return false;
195
+ }
196
+ if (nameSel && d.name.toLowerCase().indexOf(nameSel) === -1){
197
+ return false;
198
+ }
199
+ return true;
200
+ });
201
+ }
202
+
203
+ var sortState = { key:'name', dir:'asc' };
204
+ function sortData(key){
205
+ if (sortState.key === key){ sortState.dir = (sortState.dir === 'asc') ? 'desc' : 'asc'; }
206
+ else {
207
+ sortState.key = key;
208
+ sortState.dir = (key === 'methodCount' || key === 'protectedCount') ? 'desc' : 'asc';
209
+ }
210
+ var dir = sortState.dir === 'asc' ? 1 : -1;
211
+ var baseList = getFilteredData();
212
+ var sorted = baseList.slice().sort(function(a,b){
213
+ var av = a[key], bv = b[key];
214
+ if (typeof av === 'string' && typeof bv === 'string'){
215
+ av = av.toLowerCase(); bv = bv.toLowerCase();
216
+ }
217
+ if (av > bv) return 1*dir;
218
+ if (av < bv) return -1*dir;
219
+ return 0;
220
+ });
221
+ renderTable(sorted);
222
+ updateHeaderIndicators();
223
+ }
224
+
225
+ function updateHeaderIndicators(){
226
+ var ths = document.querySelectorAll('th.sortable');
227
+ Array.prototype.forEach.call(ths, function(th){
228
+ th.classList.remove('sorted-asc','sorted-desc');
229
+ var ind = th.querySelector('.sort-ind');
230
+ if (ind) ind.textContent = '';
231
+ if (th.getAttribute('data-key') === sortState.key){
232
+ th.classList.add(sortState.dir === 'asc' ? 'sorted-asc' : 'sorted-desc');
233
+ if (ind) ind.textContent = sortState.dir === 'asc' ? '▲' : '▼';
234
+ }
235
+ });
236
+ }
237
+
238
+ function renderTable(list){
239
+ rows.innerHTML = '';
240
+ (list||[]).forEach(function(item){
241
+ var tr = document.createElement('tr');
242
+ function td(html, cls){
243
+ var c=document.createElement('td');
244
+ if(cls){c.className=cls;}
245
+ c.innerHTML = html||'';
246
+ return c;
247
+ }
248
+
249
+ // Plugin name
250
+ tr.appendChild(td('<strong>'+item.name+'</strong>'));
251
+
252
+ // Path
253
+ tr.appendChild(td(item.path ? ('<code>'+item.path+'</code>') : '<span class="small">n/a</span>'));
254
+
255
+ // Async method count
256
+ tr.appendChild(td(String(item.methodCount), 't-center'));
257
+
258
+ // Methods list (all top-level + async). Color-code:
259
+ // - async methods: green chip
260
+ // - protected methods: red chip
261
+ var methodsHTML;
262
+ if (item.methodsAll && item.methodsAll.length){
263
+ methodsHTML = '<div class="chips">'+item.methodsAll.map(function(m){
264
+ var classes = ['chip'];
265
+ if (item.asyncMethods && item.asyncMethods.indexOf(m) !== -1){
266
+ classes.push('async');
267
+ }
268
+ if (item.protected && item.protected.indexOf(m) !== -1){
269
+ classes.push('protected');
270
+ }
271
+ return '<span class="'+classes.join(' ')+'">'+m+'</span>';
272
+ }).join(' ') + '</div>';
273
+ } else {
274
+ methodsHTML = '<span class="small">None</span>';
275
+ }
276
+ tr.appendChild(td(methodsHTML));
277
+
278
+ // Apps using
279
+ var appsHTML = item.usedBy.length
280
+ ? '<div class="chips">'+item.usedBy.map(function(app){
281
+ var cls = 'chip app';
282
+ var link = '/JOE/'+app;
283
+ return '<a class="'+cls+'" href="'+link+'" target="_app_'+app+'">'+app+'</a>';
284
+ }).join(' ')+'</div>'
285
+ : '<span class="small">None</span>';
286
+ tr.appendChild(td(appsHTML));
287
+
288
+ // Protected list
289
+ var protHTML = item.protectedCount
290
+ ? '<div class="chips">'+item.protected.map(function(m){ return '<span class="chip">'+m+'</span>'; }).join(' ')+'</div>'
291
+ : '<span class="small">None</span>';
292
+ tr.appendChild(td(protHTML, 't-center'));
293
+
294
+ // Notes
295
+ var notes = [];
296
+ if (item.override){
297
+ notes.push('<span class="badge warn">override</span>');
298
+ }
299
+ if (item.name === 'chatgpt'){
300
+ notes.push('<span class="badge info">AI / Responses+MCP</span>');
301
+ }
302
+ tr.appendChild(td(notes.join(' ') || '<span class="small">—</span>'));
303
+
304
+ rows.appendChild(tr);
305
+ });
306
+ }
307
+
308
+ // Attach header sort handlers once
309
+ Array.prototype.forEach.call(document.querySelectorAll('th.sortable'), function(th){
310
+ th.onclick = function(){ sortData(th.getAttribute('data-key')); };
311
+ });
312
+
313
+ if (appFilterSel){
314
+ appFilterSel.onchange = function(){ sortData(sortState.key || 'name'); };
315
+ }
316
+ if (nameFilter){
317
+ nameFilter.oninput = function(){ sortData(sortState.key || 'name'); };
318
+ }
319
+
320
+ sortData('name');
321
+ setStatus('Loaded '+Object.keys(plugins||{}).length+' plugins', true);
322
+ }catch(e){
323
+ setStatus(e.message||String(e), false);
324
+ }
325
+ }
326
+
327
+ refresh.onclick = load;
328
+ setTimeout(load, 50);
329
+ })();
330
+ </script>
331
+ </body>
332
+ </html>
333
+
@@ -345,7 +345,7 @@ When configuring a JOE‑aware Custom GPT or dev agent, include the following do
345
345
  - **Core architecture & instance info**
346
346
  - `README.md` – especially:
347
347
  - “Architecture & Mental Model (Server)”
348
- - MCP overview + test instructions
348
+ - MCP overview + test instructions (including references to `_www/mcp-test.html`, `_www/mcp-schemas.html`, and `_www/plugins-test.html` for live inspection)
349
349
  - `CHANGELOG.md` – AI/MCP‑related entries (0.10.43x+, 0.10.50x+, 0.10.62x+), already summarize important AI behavior.
350
350
  - **Agent / instruction docs (existing)**
351
351
  - `docs/JOE_Master_Knowledge_Export.md` – master context dump for schemas and concepts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-object-editor",
3
- "version": "0.10.662",
3
+ "version": "0.10.663",
4
4
  "description": "JOE the Json Object Editor | Platform Edition",
5
5
  "main": "app.js",
6
6
  "scripts": {
package/readme.md CHANGED
@@ -4,6 +4,10 @@
4
4
  JOE is software that allows you to manage data models via JSON objects. There are two flavors, the client-side version and nodejs server platform.
5
5
 
6
6
 
7
+ ## What's new in 0.10.663 (brief)
8
+ - Plugin Inventory debug page:
9
+ - New `_www/plugins-test.html` page (linked from the MCP nav) shows all active plugins, their async/top-level methods, and which JOE apps reference them. It’s auth-protected and backed by the `plugin-utils` plugin, and is especially useful for AI agents and developers to confirm that methods like `chatgpt.autofill` and `chatgpt.widgetStart` are present on a given instance.
10
+
7
11
  ## What's new in 0.10.662 (brief)
8
12
  - React Form Integration with JSON Definitions:
9
13
  - **JSON includes**: `include` schema now supports `filetype: 'json'` for storing JSON form definitions (served at `/_include/{id}` with proper content-type).
@@ -149,30 +149,51 @@ server.get(['/terms','/terms-of-service'],function(req,res){
149
149
  <p>For questions, contact ${contact}.</p>
150
150
  </body></html>`);
151
151
  });
152
- // Secure MCP test/export pages with standard auth (root and JOEPATH paths)
153
- server.get('/mcp-test.html',auth,function(req,res){
152
+ // Secure MCP / AI debug pages with standard auth (root and JOEPATH paths)
153
+ server.get('/mcp-test.html', auth, function(req,res){
154
154
  res.sendFile(path.join(JOE.joedir,'_www','mcp-test.html'));
155
155
  });
156
- server.get('/mcp-export.html',auth,function(req,res){
156
+ server.get('/mcp-export.html', auth, function(req,res){
157
157
  res.sendFile(path.join(JOE.joedir,'_www','mcp-export.html'));
158
158
  });
159
- server.get('/mcp-schemas.html',auth,function(req,res){
159
+ server.get('/mcp-schemas.html', auth, function(req,res){
160
160
  res.sendFile(path.join(JOE.joedir,'_www','mcp-schemas.html'));
161
161
  });
162
- server.get(JOE.webconfig.joepath+'_www/mcp-test.html',auth,function(req,res){
162
+ server.get('/mcp-prompt.html', auth, function(req,res){
163
+ res.sendFile(path.join(JOE.joedir,'_www','mcp-prompt.html'));
164
+ });
165
+ server.get('/matrix.html', auth, function(req,res){
166
+ res.sendFile(path.join(JOE.joedir,'_www','matrix.html'));
167
+ });
168
+ server.get('/ai-widget-test.html', auth, function(req,res){
169
+ res.sendFile(path.join(JOE.joedir,'_www','ai-widget-test.html'));
170
+ });
171
+ server.get('/plugins-test.html', auth, function(req,res){
172
+ res.sendFile(path.join(JOE.joedir,'_www','plugins-test.html'));
173
+ });
174
+
175
+ // Auth-protected variants under the JOE path as well
176
+ server.get(JOE.webconfig.joepath + '_www/mcp-test.html', auth, function(req,res){
163
177
  res.sendFile(path.join(JOE.joedir,'_www','mcp-test.html'));
164
178
  });
165
- server.get(JOE.webconfig.joepath+'_www/mcp-export.html',auth,function(req,res){
179
+ server.get(JOE.webconfig.joepath + '_www/mcp-export.html', auth, function(req,res){
166
180
  res.sendFile(path.join(JOE.joedir,'_www','mcp-export.html'));
167
181
  });
168
- server.get(JOE.webconfig.joepath+'_www/mcp-schemas.html',auth,function(req,res){
182
+ server.get(JOE.webconfig.joepath + '_www/mcp-schemas.html', auth, function(req,res){
169
183
  res.sendFile(path.join(JOE.joedir,'_www','mcp-schemas.html'));
170
184
  });
171
-
172
- // AI Widget test page (Responses + assistants) – auth protected
173
- server.get(['/ai-widget-test.html', JOE.webconfig.joepath + 'ai-widget-test.html'], auth, function(req,res){
185
+ server.get(JOE.webconfig.joepath + '_www/mcp-prompt.html', auth, function(req,res){
186
+ res.sendFile(path.join(JOE.joedir,'_www','mcp-prompt.html'));
187
+ });
188
+ server.get(JOE.webconfig.joepath + 'matrix.html', auth, function(req,res){
189
+ res.sendFile(path.join(JOE.joedir,'_www','matrix.html'));
190
+ });
191
+ server.get(JOE.webconfig.joepath + 'ai-widget-test.html', auth, function(req,res){
174
192
  res.sendFile(path.join(JOE.joedir,'_www','ai-widget-test.html'));
175
193
  });
194
+ server.get(JOE.webconfig.joepath + 'plugins-test.html', auth, function(req,res){
195
+ res.sendFile(path.join(JOE.joedir,'_www','plugins-test.html'));
196
+ });
176
197
 
177
198
  server.use(JOE.webconfig.joepath,express.static(JOE.joedir));
178
199
 
@@ -2,17 +2,35 @@ function PluginUtils(){
2
2
  var self = this;
3
3
 
4
4
  function getPluginList(){
5
- const plugins = {}
6
- for(var plug in JOE.Apps.plugins){
5
+ const plugins = {};
6
+ for (var plug in JOE.Apps.plugins){
7
+ if (!Object.prototype.hasOwnProperty.call(JOE.Apps.plugins, plug)) { continue; }
7
8
  let plugin = JOE.Apps.plugins[plug];
8
- plugins[plug]={
9
- name:plug,
10
- protected:plugin.protected,
11
- async:Object.keys(plugin.async||{}),
12
- _pathname:plugin._pathname,
13
- _override:plugin._override,
14
- }
15
- };
9
+ if (!plugin) { continue; }
10
+
11
+ const asyncNames = Object.keys(plugin.async || {});
12
+ const protectedNames = Array.isArray(plugin.protected) ? plugin.protected : [];
13
+
14
+ // Discover top-level function methods on the plugin object. We skip:
15
+ // - internal metadata fields like 'async' and 'protected'
16
+ // - properties starting with '_' (conventionally private)
17
+ // - non-functions
18
+ const methodNames = Object.keys(plugin).filter(function (k){
19
+ if (k === 'async' || k === 'protected') { return false; }
20
+ if (k[0] === '_') { return false; }
21
+ if (typeof plugin[k] !== 'function') { return false; }
22
+ return true;
23
+ });
24
+
25
+ plugins[plug] = {
26
+ name: plug,
27
+ protected: protectedNames,
28
+ async: asyncNames,
29
+ methods: methodNames,
30
+ _pathname: plugin._pathname,
31
+ _override: plugin._override
32
+ };
33
+ }
16
34
  return plugins;
17
35
  }
18
36
  this.default = function(data,req,res){
@@ -44,7 +44,7 @@ var user = function(){return{
44
44
  'name',
45
45
  'info',
46
46
  {section_start:'content'},
47
- {name:'content',type:'code', label:false,height:'600px',language:function(item){
47
+ {name:'content',type:'code', label:false,height:'600px',relaodable:true,language:function(item){
48
48
  switch(item.filetype){
49
49
  case 'js':
50
50
  case 'javascript':