json-object-editor 0.10.625 → 0.10.633
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 +6 -0
- package/_www/ai-widget-test.html +367 -0
- package/_www/mcp-test.html +10 -1
- package/css/joe-styles.css +11 -3
- package/css/joe.css +12 -4
- package/css/joe.min.css +1 -1
- package/docs/joe_agent_custom_gpt_instructions_v_3.md +9 -0
- package/dummy +10 -0
- package/img/svgs/ai_assistant.svg +1 -0
- package/img/svgs/ai_assistant_white.svg +1 -0
- package/js/JsonObjectEditor.jquery.craydent.js +34 -3
- package/js/joe-ai.js +784 -52
- package/js/joe.js +52 -21
- package/js/joe.min.js +1 -1
- package/package.json +1 -1
- package/readme.md +8 -1
- package/server/apps/aihub.js +97 -0
- package/server/fields/core.js +4 -1
- package/server/modules/MCP.js +233 -2
- package/server/modules/Server.js +1 -46
- package/server/plugins/auth.js +34 -30
- package/server/plugins/chatgpt-assistants.js +70 -35
- package/server/plugins/chatgpt.js +560 -44
- package/server/schemas/ai_assistant.js +149 -1
- package/server/schemas/ai_conversation.js +14 -1
- package/server/schemas/ai_widget_conversation.js +133 -14
- package/server/schemas/project.js +27 -3
- package/server/schemas/task.js +6 -3
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -660,4 +660,11 @@ To help you develop and debug the widget + plugin in your instance, JOE exposes
|
|
|
660
660
|
<a href="/ai-widget-test.html" target="ai_widget_test_win" rel="noopener">AI Widget</a>
|
|
661
661
|
```
|
|
662
662
|
|
|
663
|
-
- This appears on MCP test/export/prompt pages and on the AI widget test page itself.
|
|
663
|
+
- This appears on MCP test/export/prompt pages and on the AI widget test page itself.
|
|
664
|
+
|
|
665
|
+
### AI / Widget Changelog (current work – `0.10.632`)
|
|
666
|
+
|
|
667
|
+
- Added a Responses‑based tool runner for `<joe-ai-widget>` that wires `ai_assistant.tools` into MCP functions via `chatgpt.runWithTools`.
|
|
668
|
+
- Enhanced widget UX: assistant/user bubble theming (using `assistant_color` and user `color`), inline “tools used this turn” meta messages, and markdown rendering for assistant replies.
|
|
669
|
+
- Expanded the AI widget test page with an assistant picker, live tool JSON viewer, a clickable conversation history list (resume existing `ai_widget_conversation` threads), and safer user handling (widget conversations now store user id/name/color explicitly and OAuth token‑exchange errors from Google are surfaced clearly during login).
|
|
670
|
+
- Added field-level AI autofill support: schemas can declare `ai` config on a field (e.g. `{ name:'ai_summary', type:'rendering', ai:{ prompt:'Summarize the project in a few sentences.' } }`), which renders an inline “AI” button that calls `_joe.Ai.populateField('ai_summary')` and posts to `/API/plugin/chatgpt/autofill` to compute a JSON `patch` and update the UI (with confirmation before overwriting non-empty values).
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
var App = function () {
|
|
2
|
+
|
|
3
|
+
this.title = 'AIHub';
|
|
4
|
+
this.description = 'Central hub for managing AI assistants, prompts, tools, conversations, and widget chats in JOE.';
|
|
5
|
+
|
|
6
|
+
// Core AI-related schemas in JOE
|
|
7
|
+
this.collections = [
|
|
8
|
+
'ai_assistant',
|
|
9
|
+
'ai_prompt',
|
|
10
|
+
'ai_tool',
|
|
11
|
+
'ai_response',
|
|
12
|
+
'ai_conversation',
|
|
13
|
+
'ai_widget_conversation'
|
|
14
|
+
].concat(JOE.webconfig.default_schemas);
|
|
15
|
+
|
|
16
|
+
this.dashboard = [
|
|
17
|
+
// Standard app home: shows AIHub schemas plus default schemas
|
|
18
|
+
JOE.Apps.Cards.appHome({ cssclass: 'w2 h3' }),
|
|
19
|
+
|
|
20
|
+
// Small MCP test portal card
|
|
21
|
+
{
|
|
22
|
+
type: 'Card',
|
|
23
|
+
config: {
|
|
24
|
+
title: 'MCP Test Portal',
|
|
25
|
+
left: 0,
|
|
26
|
+
top: 3,
|
|
27
|
+
cssclass: 'w2 h1',
|
|
28
|
+
content: function () {
|
|
29
|
+
return '' +
|
|
30
|
+
'<div class="spaced">' +
|
|
31
|
+
'<div><b>MCP Tools & Tests</b></div>' +
|
|
32
|
+
'<ul style="margin:6px 0 0 16px; padding:0; list-style:disc;">' +
|
|
33
|
+
'<li><a href="/mcp-test.html" target="mcp_test_win" rel="noopener">MCP Test</a></li>' +
|
|
34
|
+
'<li><a href="/mcp-export.html" target="mcp_export_win" rel="noopener">MCP Export</a></li>' +
|
|
35
|
+
'<li><a href="/mcp-schemas.html" target="mcp_schemas_win" rel="noopener">Schemas</a></li>' +
|
|
36
|
+
'<li><a href="/mcp-prompt.html" target="mcp_prompt_win" rel="noopener">MCP Prompt</a></li>' +
|
|
37
|
+
'<li><a href="/ai-widget-test.html" target="ai_widget_test_win">AI Widget</a></li>'+
|
|
38
|
+
'</ul>' +
|
|
39
|
+
'</div>';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// Cap card with an embedded joe-ai-widget that fills the panel
|
|
45
|
+
{
|
|
46
|
+
type: 'Card',
|
|
47
|
+
config: {
|
|
48
|
+
title: 'AIHub Chat',
|
|
49
|
+
left: 2,
|
|
50
|
+
top: 0,
|
|
51
|
+
cssclass: 'w4 h4',
|
|
52
|
+
content: function () {
|
|
53
|
+
return '' +
|
|
54
|
+
'<div style="width:100%;height:100%;display:flex;flex-direction:column;">' +
|
|
55
|
+
'<joe-ai-assistant-picker for_widget="aihub_widget"></joe-ai-assistant-picker>' +
|
|
56
|
+
'<joe-ai-widget ' +
|
|
57
|
+
'id="aihub_widget" ' +
|
|
58
|
+
'title="AIHub Assistant" ' +
|
|
59
|
+
'source="aihub_card" ' +
|
|
60
|
+
'user_id="' + ((_joe && _joe.User && _joe.User._id) || '') + '" ' +
|
|
61
|
+
'style="flex:1 1 auto;width:100%;height:100%;">' +
|
|
62
|
+
'</joe-ai-widget>' +
|
|
63
|
+
'</div>';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// Widget conversations list (replaces Recently Updated AI Items card for now)
|
|
69
|
+
{
|
|
70
|
+
type: 'Card',
|
|
71
|
+
config: {
|
|
72
|
+
title: 'Widget Conversations',
|
|
73
|
+
left: 6,
|
|
74
|
+
top: 1,
|
|
75
|
+
cssclass: 'w2 h3',
|
|
76
|
+
content: function () {
|
|
77
|
+
return '' +
|
|
78
|
+
'<div style="width:100%;height:100%;display:flex;flex-direction:column;">' +
|
|
79
|
+
'<joe-ai-conversation-list ' +
|
|
80
|
+
'for_widget="aihub_widget" ' +
|
|
81
|
+
'source="aihub_card">' +
|
|
82
|
+
'</joe-ai-conversation-list>' +
|
|
83
|
+
'</div>';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// Platform/system stats in the context of AIHub
|
|
89
|
+
JOE.Apps.Cards.systemStats({ top: 0, left: 6 })
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
return this;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
module.exports = new App();
|
|
96
|
+
|
|
97
|
+
|
package/server/fields/core.js
CHANGED
|
@@ -546,7 +546,10 @@ var fields = {
|
|
|
546
546
|
return `_joe.Ai.spawnChatHelper('${object._id}');`;
|
|
547
547
|
},
|
|
548
548
|
|
|
549
|
-
}
|
|
549
|
+
},
|
|
550
|
+
listConversations:{display:'Ai Conversations', type:"content",reloadable:true,run:function(obj){
|
|
551
|
+
return _joe.schemas.ai_conversation.methods.listConversations(obj,true);
|
|
552
|
+
}},
|
|
550
553
|
|
|
551
554
|
};
|
|
552
555
|
|
package/server/modules/MCP.js
CHANGED
|
@@ -7,12 +7,21 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const MCP = {};
|
|
10
|
-
const { Storage, Schemas } = global.JOE; // Adjust as needed based on how your modules are wired
|
|
11
10
|
|
|
12
11
|
// Internal helpers
|
|
12
|
+
function getStorage() {
|
|
13
|
+
return (global.JOE && global.JOE.Storage) || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getSchemas() {
|
|
17
|
+
return (global.JOE && global.JOE.Schemas) || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
function loadFromStorage(collection, query) {
|
|
14
21
|
return new Promise((resolve, reject) => {
|
|
15
22
|
try {
|
|
23
|
+
const Storage = getStorage();
|
|
24
|
+
if (!Storage) return reject(new Error('Storage module not initialized'));
|
|
16
25
|
Storage.load(collection, query || {}, function(err, results){
|
|
17
26
|
if (err) return reject(err);
|
|
18
27
|
resolve(results || []);
|
|
@@ -38,6 +47,60 @@ function sanitizeItems(items) {
|
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
49
|
|
|
50
|
+
// Resolve simple dotted paths against an object, including arrays.
|
|
51
|
+
// Example: getPathValues(recipe, "ingredients.id") → ["ing1","ing2",...]
|
|
52
|
+
function getPathValues(root, path) {
|
|
53
|
+
if (!root || !path) return [];
|
|
54
|
+
const parts = String(path).split('.');
|
|
55
|
+
let current = [root];
|
|
56
|
+
for (let i = 0; i < parts.length; i++) {
|
|
57
|
+
const key = parts[i];
|
|
58
|
+
const next = [];
|
|
59
|
+
for (let j = 0; j < current.length; j++) {
|
|
60
|
+
const val = current[j];
|
|
61
|
+
if (val == null) continue;
|
|
62
|
+
if (Array.isArray(val)) {
|
|
63
|
+
val.forEach(function (item) {
|
|
64
|
+
if (item && Object.prototype.hasOwnProperty.call(item, key)) {
|
|
65
|
+
next.push(item[key]);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
} else if (Object.prototype.hasOwnProperty.call(val, key)) {
|
|
69
|
+
next.push(val[key]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
current = next;
|
|
73
|
+
if (!current.length) break;
|
|
74
|
+
}
|
|
75
|
+
// Flatten one level in case the last hop produced arrays
|
|
76
|
+
const out = [];
|
|
77
|
+
current.forEach(function (v) {
|
|
78
|
+
if (Array.isArray(v)) {
|
|
79
|
+
v.forEach(function (x) { if (x != null) out.push(x); });
|
|
80
|
+
} else if (v != null) {
|
|
81
|
+
out.push(v);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Best-effort helper to get a normalized schema summary for a given name.
|
|
88
|
+
// Prefers the precomputed `Schemas.summary[name]` map, but falls back to
|
|
89
|
+
// `Schemas.schema[name].summary` when the summary map has not been generated
|
|
90
|
+
// or that particular schema has not yet been merged in.
|
|
91
|
+
function getSchemaSummary(name) {
|
|
92
|
+
if (!name) return null;
|
|
93
|
+
const Schemas = getSchemas();
|
|
94
|
+
if (!Schemas) return null;
|
|
95
|
+
if (Schemas.summary && Schemas.summary[name]) {
|
|
96
|
+
return Schemas.summary[name];
|
|
97
|
+
}
|
|
98
|
+
if (Schemas.schema && Schemas.schema[name] && Schemas.schema[name].summary) {
|
|
99
|
+
return Schemas.schema[name].summary;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
41
104
|
function getComparable(val){
|
|
42
105
|
if (val == null) return null;
|
|
43
106
|
// Date-like
|
|
@@ -84,6 +147,7 @@ MCP.tools = {
|
|
|
84
147
|
|
|
85
148
|
// List all schema names in the system
|
|
86
149
|
listSchemas: async (_params, _ctx) => {
|
|
150
|
+
var Schemas = getSchemas();
|
|
87
151
|
const list = (Schemas && (
|
|
88
152
|
(Schemas.schemaList && Schemas.schemaList.length && Schemas.schemaList) ||
|
|
89
153
|
(Schemas.schema && Object.keys(Schemas.schema))
|
|
@@ -146,6 +210,156 @@ MCP.tools = {
|
|
|
146
210
|
return sanitizeItems(obj)[0];
|
|
147
211
|
},
|
|
148
212
|
|
|
213
|
+
/**
|
|
214
|
+
* understandObject
|
|
215
|
+
*
|
|
216
|
+
* High-level helper for agents: given an _id (and optional itemtype),
|
|
217
|
+
* returns a rich payload combining:
|
|
218
|
+
* - object: the raw object (ids intact)
|
|
219
|
+
* - flattened: the same object flattened to a limited depth
|
|
220
|
+
* - schemas: a map of schema summaries for the main itemtype and any
|
|
221
|
+
* referenced itemtypes (keyed by schema name)
|
|
222
|
+
* - related: an array of referenced objects discovered via outbound
|
|
223
|
+
* relationships in the schema summary.
|
|
224
|
+
*
|
|
225
|
+
* When `slim` is false (default), each related entry includes both `object`
|
|
226
|
+
* and `flattened`. When `slim` is true, only the main object is flattened
|
|
227
|
+
* and related entries are reduced to slim references:
|
|
228
|
+
* { field, _id, itemtype, object: { _id, itemtype, name, info } }
|
|
229
|
+
*
|
|
230
|
+
* Agents should prefer this tool when they need to understand or work with
|
|
231
|
+
* an object by id, instead of issuing many individual getObject / getSchema
|
|
232
|
+
* calls. The original object always keeps its reference ids; expanded views
|
|
233
|
+
* live under `flattened` and `related[*]`.
|
|
234
|
+
*/
|
|
235
|
+
understandObject: async ({ _id, itemtype, schema, depth = 2, slim = false } = {}, _ctx) => {
|
|
236
|
+
if (!_id) throw new Error("Missing required param '_id'");
|
|
237
|
+
itemtype = itemtype || schema;
|
|
238
|
+
|
|
239
|
+
// Base object (sanitized) without flattening
|
|
240
|
+
const base = await MCP.tools.getObject({ _id, itemtype, flatten: false }, _ctx);
|
|
241
|
+
const mainType = base.itemtype || itemtype || null;
|
|
242
|
+
|
|
243
|
+
const result = {
|
|
244
|
+
_id: base._id,
|
|
245
|
+
itemtype: mainType,
|
|
246
|
+
object: base,
|
|
247
|
+
flattened: null,
|
|
248
|
+
schemas: {},
|
|
249
|
+
related: [],
|
|
250
|
+
// Deduped lookups for global reference types
|
|
251
|
+
tags: {},
|
|
252
|
+
statuses: {},
|
|
253
|
+
slim: !!slim
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Main schema summary
|
|
257
|
+
const mainSummary = getSchemaSummary(mainType);
|
|
258
|
+
if (mainType && mainSummary) {
|
|
259
|
+
result.schemas[mainType] = mainSummary;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Flattened view of the main object (depth-limited)
|
|
263
|
+
if (JOE && JOE.Utils && JOE.Utils.flattenObject) {
|
|
264
|
+
try {
|
|
265
|
+
const flat = JOE.Utils.flattenObject(base._id, { recursive: true, depth });
|
|
266
|
+
result.flattened = sanitizeItems(flat)[0];
|
|
267
|
+
} catch (_e) {
|
|
268
|
+
result.flattened = null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const seenSchemas = new Set(Object.keys(result.schemas || {}));
|
|
273
|
+
function addSchemaIfPresent(name) {
|
|
274
|
+
if (!name || seenSchemas.has(name)) return;
|
|
275
|
+
const sum = getSchemaSummary(name);
|
|
276
|
+
if (sum) {
|
|
277
|
+
result.schemas[name] = sum;
|
|
278
|
+
seenSchemas.add(name);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Discover outbound relationships from schema summary
|
|
283
|
+
const schemaSummary = mainType && getSchemaSummary(mainType);
|
|
284
|
+
const outbound = (schemaSummary &&
|
|
285
|
+
schemaSummary.relationships &&
|
|
286
|
+
Array.isArray(schemaSummary.relationships.outbound))
|
|
287
|
+
? schemaSummary.relationships.outbound
|
|
288
|
+
: [];
|
|
289
|
+
|
|
290
|
+
for (let i = 0; i < outbound.length; i++) {
|
|
291
|
+
const rel = outbound[i] || {};
|
|
292
|
+
const field = rel.field;
|
|
293
|
+
const targetSchema = rel.targetSchema;
|
|
294
|
+
if (!field || !targetSchema) continue;
|
|
295
|
+
|
|
296
|
+
// Support nested paths like "ingredients.id" coming from objectList fields.
|
|
297
|
+
const vals = getPathValues(base, field);
|
|
298
|
+
if (!vals || !vals.length) continue;
|
|
299
|
+
|
|
300
|
+
const ids = Array.isArray(vals) ? vals : [vals];
|
|
301
|
+
for (let j = 0; j < ids.length; j++) {
|
|
302
|
+
const rid = ids[j];
|
|
303
|
+
if (!rid) continue;
|
|
304
|
+
let robj = null;
|
|
305
|
+
try {
|
|
306
|
+
robj = await MCP.tools.getObject({ _id: rid, itemtype: targetSchema, flatten: false }, _ctx);
|
|
307
|
+
} catch (_e) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (!robj) continue;
|
|
311
|
+
|
|
312
|
+
const rType = robj.itemtype || targetSchema;
|
|
313
|
+
// Global reference types (tag/status) go into top-level lookup maps
|
|
314
|
+
if (rType === 'tag' || rType === 'status') {
|
|
315
|
+
const mapName = (rType === 'tag') ? 'tags' : 'statuses';
|
|
316
|
+
if (!result[mapName][robj._id]) {
|
|
317
|
+
result[mapName][robj._id] = {
|
|
318
|
+
_id: robj._id,
|
|
319
|
+
itemtype: rType,
|
|
320
|
+
name: robj.name || robj.label || robj.info || ''
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
if (slim) {
|
|
325
|
+
const slimObj = toSlim(robj);
|
|
326
|
+
result.related.push({
|
|
327
|
+
field,
|
|
328
|
+
_id: slimObj._id,
|
|
329
|
+
itemtype: slimObj.itemtype || rType,
|
|
330
|
+
object: {
|
|
331
|
+
_id: slimObj._id,
|
|
332
|
+
itemtype: slimObj.itemtype || rType,
|
|
333
|
+
name: slimObj.name,
|
|
334
|
+
info: slimObj.info
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
} else {
|
|
338
|
+
let rflat = null;
|
|
339
|
+
if (JOE && JOE.Utils && JOE.Utils.flattenObject) {
|
|
340
|
+
try {
|
|
341
|
+
const f = JOE.Utils.flattenObject(robj._id, { recursive: true, depth: Math.max(1, depth - 1) });
|
|
342
|
+
rflat = sanitizeItems(f)[0];
|
|
343
|
+
} catch (_e) {
|
|
344
|
+
rflat = null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
result.related.push({
|
|
348
|
+
field,
|
|
349
|
+
_id: robj._id,
|
|
350
|
+
itemtype: rType,
|
|
351
|
+
object: robj,
|
|
352
|
+
flattened: rflat
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
addSchemaIfPresent(rType || targetSchema);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return result;
|
|
361
|
+
},
|
|
362
|
+
|
|
149
363
|
/* Deprecated: use unified 'search' instead
|
|
150
364
|
getObjectsByIds: async () => { throw new Error('Use search with ids instead'); },
|
|
151
365
|
queryObjects: async () => { throw new Error('Use search instead'); },
|
|
@@ -249,6 +463,8 @@ MCP.tools = {
|
|
|
249
463
|
|
|
250
464
|
// Save an object via Storage (respects events/history)
|
|
251
465
|
saveObject: async ({ object }, ctx = {}) => {
|
|
466
|
+
const Storage = getStorage();
|
|
467
|
+
if (!Storage) throw new Error('Storage module not initialized');
|
|
252
468
|
if (!object || !object.itemtype) throw new Error("'object' with 'itemtype' is required");
|
|
253
469
|
const user = (ctx.req && ctx.req.User) ? ctx.req.User : { name: 'anonymous' };
|
|
254
470
|
// Ensure server-side update timestamp parity with /API/save
|
|
@@ -270,6 +486,8 @@ MCP.tools = {
|
|
|
270
486
|
|
|
271
487
|
// Batch save with bounded concurrency; preserves per-item history/events
|
|
272
488
|
saveObjects: async ({ objects, stopOnError = false, concurrency = 5 } = {}, ctx = {}) => {
|
|
489
|
+
const Storage = getStorage();
|
|
490
|
+
if (!Storage) throw new Error('Storage module not initialized');
|
|
273
491
|
if (!Array.isArray(objects) || objects.length === 0) {
|
|
274
492
|
throw new Error("'objects' (non-empty array) is required");
|
|
275
493
|
}
|
|
@@ -325,6 +543,7 @@ MCP.tools = {
|
|
|
325
543
|
|
|
326
544
|
// Hydration: surface core fields (from file), all schemas, statuses, and tags (no params)
|
|
327
545
|
hydrate: async (_params, _ctx) => {
|
|
546
|
+
var Schemas = getSchemas();
|
|
328
547
|
let coreDef = (JOE && JOE.Fields && JOE.Fields['core']) || null;
|
|
329
548
|
if (!coreDef) {
|
|
330
549
|
try { coreDef = require(__dirname + '/../fields/core.js'); } catch (_e) { coreDef = {}; }
|
|
@@ -387,7 +606,8 @@ MCP.descriptions = {
|
|
|
387
606
|
saveObject: "Create/update an object; triggers events/history.",
|
|
388
607
|
saveObjects: "Batch save objects with bounded concurrency; per-item history/events preserved.",
|
|
389
608
|
hydrate: "Loads and merges the full JOE context, including core and organization-specific schemas, relationships, universal fields (tags and statuses), and datasets. Returns a single unified object describing the active environment for use by agents, UIs, and plugins.",
|
|
390
|
-
listApps: "List app definitions (title, description, collections, plugins)."
|
|
609
|
+
listApps: "List app definitions (title, description, collections, plugins).",
|
|
610
|
+
understandObject: "High-level helper: given an _id (and optional itemtype), returns { object, flattened, schemas, related[] } combining the main object, its schema summary, and referenced objects plus their schemas. Prefer this when you need to understand or reason about an object by id instead of issuing many separate getObject/getSchema calls."
|
|
391
611
|
};
|
|
392
612
|
|
|
393
613
|
MCP.params = {
|
|
@@ -440,6 +660,17 @@ MCP.params = {
|
|
|
440
660
|
},
|
|
441
661
|
required: []
|
|
442
662
|
},
|
|
663
|
+
understandObject: {
|
|
664
|
+
type: "object",
|
|
665
|
+
properties: {
|
|
666
|
+
_id: { type: "string" },
|
|
667
|
+
itemtype: { type: "string" },
|
|
668
|
+
schema: { type: "string" },
|
|
669
|
+
depth: { type: "integer" },
|
|
670
|
+
slim: { type: "boolean" }
|
|
671
|
+
},
|
|
672
|
+
required: ["_id"]
|
|
673
|
+
},
|
|
443
674
|
fuzzySearch: {
|
|
444
675
|
type: "object",
|
|
445
676
|
properties: {
|
package/server/modules/Server.js
CHANGED
|
@@ -171,52 +171,7 @@ server.get(JOE.webconfig.joepath+'_www/mcp-schemas.html',auth,function(req,res){
|
|
|
171
171
|
|
|
172
172
|
// AI Widget test page (Responses + assistants) – auth protected
|
|
173
173
|
server.get(['/ai-widget-test.html', JOE.webconfig.joepath + 'ai-widget-test.html'], auth, function(req,res){
|
|
174
|
-
|
|
175
|
-
if (!assistantId && JOE && JOE.Utils && JOE.Utils.Settings) {
|
|
176
|
-
try {
|
|
177
|
-
var settingObj = JOE.Utils.Settings('DEFAULT_AI_ASSISTANT', { object: true });
|
|
178
|
-
assistantId = (settingObj && settingObj.value) || '';
|
|
179
|
-
} catch(e) {
|
|
180
|
-
console.log('[ai-widget-test] error reading DEFAULT_AI_ASSISTANT:', e && e.message);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
var joePath = (JOE && JOE.webconfig && JOE.webconfig.joepath) || '/JsonObjectEditor/';
|
|
184
|
-
var assistantAttr = assistantId ? ' ai_assistant_id="'+assistantId.replace(/"/g,'"')+'"' : '';
|
|
185
|
-
|
|
186
|
-
res.send(`<!doctype html>
|
|
187
|
-
<html>
|
|
188
|
-
<head>
|
|
189
|
-
<meta charset="utf-8">
|
|
190
|
-
<title>JOE AI Widget Test</title>
|
|
191
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
192
|
-
<style>
|
|
193
|
-
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:20px;background:#f3f4f6;}
|
|
194
|
-
h1{margin-top:0;}
|
|
195
|
-
.small{font-size:13px;color:#6b7280;margin-bottom:16px;}
|
|
196
|
-
.container{max-width:480px;margin:0 auto;background:#fff;border-radius:12px;box-shadow:0 4px 14px rgba(0,0,0,0.06);padding:16px;}
|
|
197
|
-
.meta{font-size:12px;color:#6b7280;margin-bottom:8px;}
|
|
198
|
-
code{background:#e5e7eb;border-radius:4px;padding:2px 4px;font-size:12px;}
|
|
199
|
-
</style>
|
|
200
|
-
</head>
|
|
201
|
-
<body>
|
|
202
|
-
<div id="mcp-nav"></div>
|
|
203
|
-
<script src="${joePath}_www/mcp-nav.js"></script>
|
|
204
|
-
|
|
205
|
-
<div class="container">
|
|
206
|
-
<h1>AI Widget Test</h1>
|
|
207
|
-
<div class="small">
|
|
208
|
-
This page mounts <code><joe-ai-widget></code> and sends messages through
|
|
209
|
-
<code>/API/plugin/chatgpt/widget*</code> using the OpenAI Responses API.
|
|
210
|
-
${assistantId ? `Using <code>DEFAULT_AI_ASSISTANT</code> (ai_assistant_id=${assistantId}).` : 'No DEFAULT_AI_ASSISTANT is set; widget will use model defaults.'}
|
|
211
|
-
You can override the assistant via <code>?assistant_id=<ai_assistant _id></code>.
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
<joe-ai-widget id="widget" title="JOE AI Assistant"${assistantAttr}></joe-ai-widget>
|
|
215
|
-
</div>
|
|
216
|
-
|
|
217
|
-
<script src="${joePath}js/joe-ai.js"></script>
|
|
218
|
-
</body>
|
|
219
|
-
</html>`);
|
|
174
|
+
res.sendFile(path.join(JOE.joedir,'_www','ai-widget-test.html'));
|
|
220
175
|
});
|
|
221
176
|
|
|
222
177
|
server.use(JOE.webconfig.joepath,express.static(JOE.joedir));
|
package/server/plugins/auth.js
CHANGED
|
@@ -22,50 +22,46 @@ function Auth(){
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
this.login = async function(data,req,res){
|
|
25
|
-
|
|
26
|
-
//
|
|
25
|
+
// Use got to exchange the Google authorization_code for tokens.
|
|
26
|
+
// We POST form-encoded params to the current Google OAuth token endpoint.
|
|
27
|
+
const gotMod = await import('got');
|
|
28
|
+
const got = gotMod.default || gotMod;
|
|
29
|
+
|
|
27
30
|
var originalUrl = data.state||'';
|
|
28
31
|
console.log(originalUrl);
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
const tokenUrl = 'https://oauth2.googleapis.com/token';
|
|
34
|
+
const options = {
|
|
31
35
|
method: 'POST',
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
// Google expects application/x-www-form-urlencoded body, not querystring.
|
|
37
|
+
form: {
|
|
34
38
|
grant_type: 'authorization_code',
|
|
35
39
|
code: data.code,
|
|
36
40
|
redirect_uri: `${JOE.webconfig.authorization.host}/API/plugin/auth/login`,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'client_secret':JOE.webconfig.authorization.client_secret
|
|
41
|
+
client_id: JOE.webconfig.authorization.client_id,
|
|
42
|
+
client_secret: JOE.webconfig.authorization.client_secret
|
|
40
43
|
},
|
|
41
|
-
headers:
|
|
42
|
-
{
|
|
44
|
+
headers: {
|
|
43
45
|
'cache-control': 'no-cache',
|
|
44
|
-
|
|
45
|
-
Accept: 'application/json',
|
|
46
|
-
|
|
46
|
+
Accept: 'application/json'
|
|
47
47
|
},
|
|
48
48
|
responseType: 'json',
|
|
49
49
|
https: {
|
|
50
50
|
rejectUnauthorized: false
|
|
51
51
|
}
|
|
52
|
-
// rejectUnauthorized:false,
|
|
53
|
-
// json: true
|
|
54
52
|
};
|
|
55
53
|
|
|
56
|
-
|
|
57
|
-
got.default(options)
|
|
58
|
-
.catch(error => {
|
|
59
|
-
res.send(error);
|
|
60
|
-
})
|
|
54
|
+
got(tokenUrl, options)
|
|
61
55
|
.then(response => {
|
|
62
|
-
const body = response.body;
|
|
56
|
+
const body = response.body || {};
|
|
63
57
|
// if (error){
|
|
64
58
|
// res.send(error);
|
|
65
59
|
// return;
|
|
66
60
|
// }
|
|
67
61
|
if (body.error){
|
|
68
|
-
|
|
62
|
+
// Bubble up Google's error payload so it's easier to diagnose
|
|
63
|
+
console.error('[auth.login] Google token error payload:', body);
|
|
64
|
+
res.status(400).send(body);
|
|
69
65
|
return;
|
|
70
66
|
}
|
|
71
67
|
//res.send(body);
|
|
@@ -91,17 +87,25 @@ function Auth(){
|
|
|
91
87
|
res.redirect(finalUrl);
|
|
92
88
|
return;
|
|
93
89
|
}
|
|
94
|
-
|
|
90
|
+
//redirect to home or gotoUrl
|
|
95
91
|
res.redirect(originalUrl || `/JOE/${User.apps[0]}`);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
})
|
|
93
|
+
.catch(error => {
|
|
94
|
+
// Log structured info so we can see status + body from Google
|
|
95
|
+
const status = error.response && error.response.statusCode;
|
|
96
|
+
const body = error.response && error.response.body;
|
|
97
|
+
console.error('[auth.login] HTTPError from Google token endpoint:', {
|
|
98
|
+
status,
|
|
99
|
+
body,
|
|
100
|
+
message: error.message
|
|
101
|
+
});
|
|
102
|
+
res.status(status || 500).send(body || {
|
|
103
|
+
error: 'oauth_token_error',
|
|
104
|
+
message: error.message
|
|
105
|
+
});
|
|
100
106
|
});
|
|
101
107
|
|
|
102
|
-
|
|
103
|
-
//return(data);
|
|
104
|
-
return({use_callback:true})
|
|
108
|
+
return({use_callback:true});
|
|
105
109
|
}
|
|
106
110
|
this.html = function(data,req,res){
|
|
107
111
|
|