json-object-editor 0.10.624 → 0.10.632
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 +15 -0
- package/_www/ai-widget-test.html +367 -0
- package/_www/mcp-export.html +5 -5
- package/_www/mcp-prompt.html +1 -1
- package/_www/mcp-test.html +14 -5
- 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 +132 -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 +35 -4
- package/js/joe.min.js +1 -1
- package/package.json +1 -1
- package/readme.md +17 -10
- package/server/apps/aihub.js +97 -0
- package/server/fields/core.js +4 -1
- package/server/modules/MCP.js +263 -26
- 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 +1 -0
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -44,12 +44,12 @@ JOE is software that allows you to manage data models via JSON objects. There ar
|
|
|
44
44
|
|
|
45
45
|
- Tools
|
|
46
46
|
- `listSchemas(name?)`, `getSchema(name)`
|
|
47
|
-
- `getObject(_id,
|
|
47
|
+
- `getObject(_id, itemtype?)` (supports optional `flatten` and `depth`)
|
|
48
48
|
- `search` (exact): unified tool for cache and storage
|
|
49
|
-
- Params: `{
|
|
50
|
-
- Defaults to cache across all collections; add `
|
|
49
|
+
- Params: `{ itemtype?, query?, ids?, source?: 'cache'|'storage', limit?, flatten?, depth? }`
|
|
50
|
+
- Defaults to cache across all collections; add `itemtype` to filter; set `source:"storage"` to query a specific collection in the DB. Runtime accepts legacy alias `schema` (maps to `itemtype`). Use `fuzzySearch` for typo-tolerant free text.
|
|
51
51
|
- `fuzzySearch` (typo-tolerant free text across weighted fields)
|
|
52
|
-
- Params: `{
|
|
52
|
+
- Params: `{ itemtype?, q, filters?, fields?, threshold?, limit?, offset?, highlight?, minQueryLength? }`
|
|
53
53
|
- Defaults: `fields` resolved from schema `searchables` (plural) if present; otherwise weights `name:0.6, info:0.3, description:0.1`. `threshold:0.5`, `limit:50`, `minQueryLength:2`.
|
|
54
54
|
- Returns: `{ items, count }`. Each item may include `_score` (0..1) and `_matches` when `highlight` is true.
|
|
55
55
|
- `saveObject({ object })`
|
|
@@ -86,17 +86,17 @@ JOE is software that allows you to manage data models via JSON objects. There ar
|
|
|
86
86
|
```
|
|
87
87
|
- fuzzySearch (cache):
|
|
88
88
|
```powershell
|
|
89
|
-
$body = @{ jsonrpc="2.0"; id="6"; method="fuzzySearch"; params=@{
|
|
89
|
+
$body = @{ jsonrpc="2.0"; id="6"; method="fuzzySearch"; params=@{ itemtype="<schemaName>"; q="st paal"; threshold=0.35; limit=10 } } | ConvertTo-Json -Depth 10
|
|
90
90
|
Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
|
|
91
91
|
```
|
|
92
92
|
- search (storage):
|
|
93
93
|
```powershell
|
|
94
|
-
$body = @{ jsonrpc="2.0"; id="4"; method="search"; params=@{
|
|
94
|
+
$body = @{ jsonrpc="2.0"; id="4"; method="search"; params=@{ itemtype="<schemaName>"; source="storage"; query=@{ }; limit=10 } } | ConvertTo-Json -Depth 10
|
|
95
95
|
Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
|
|
96
96
|
```
|
|
97
97
|
- search (ids + flatten):
|
|
98
98
|
```powershell
|
|
99
|
-
$body = @{ jsonrpc="2.0"; id="5"; method="search"; params=@{
|
|
99
|
+
$body = @{ jsonrpc="2.0"; id="5"; method="search"; params=@{ itemtype="<schemaName>"; ids=@("<id1>","<id2>"); flatten=$true; depth=2 } } | ConvertTo-Json -Depth 10
|
|
100
100
|
Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
|
|
101
101
|
```
|
|
102
102
|
- saveObject:
|
|
@@ -131,13 +131,13 @@ JOE is software that allows you to manage data models via JSON objects. There ar
|
|
|
131
131
|
```bash
|
|
132
132
|
curl -s -X POST http://localhost:<PORT>/mcp \
|
|
133
133
|
-H 'Content-Type: application/json' \
|
|
134
|
-
-d '{"jsonrpc":"2.0","id":"6","method":"fuzzySearch","params":{"
|
|
134
|
+
-d '{"jsonrpc":"2.0","id":"6","method":"fuzzySearch","params":{"itemtype":"<schemaName>","q":"st paal","threshold":0.35,"limit":10}}' | jq
|
|
135
135
|
```
|
|
136
136
|
- search (storage):
|
|
137
137
|
```bash
|
|
138
138
|
curl -s -X POST http://localhost:<PORT>/mcp \
|
|
139
139
|
-H 'Content-Type: application/json' \
|
|
140
|
-
-d '{"jsonrpc":"2.0","id":"4","method":"search","params":{"
|
|
140
|
+
-d '{"jsonrpc":"2.0","id":"4","method":"search","params":{"itemtype":"<schemaName>","source":"storage","query":{},"limit":10}}' | jq
|
|
141
141
|
```
|
|
142
142
|
```bash
|
|
143
143
|
curl -s -X POST http://localhost:<PORT>/mcp \
|
|
@@ -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))
|
|
@@ -125,25 +189,177 @@ MCP.tools = {
|
|
|
125
189
|
}
|
|
126
190
|
},
|
|
127
191
|
|
|
128
|
-
// Convenience: fetch a single object by _id (
|
|
129
|
-
|
|
192
|
+
// Convenience: fetch a single object by _id (itemtype optional). Prefer cache; fallback to storage.
|
|
193
|
+
// Accepts legacy alias 'schema' for itemtype.
|
|
194
|
+
getObject: async ({ _id, itemtype, schema, flatten = false, depth = 1 }, _ctx) => {
|
|
130
195
|
if (!_id) throw new Error("Missing required param '_id'");
|
|
196
|
+
itemtype = itemtype || schema; // legacy alias
|
|
131
197
|
// Fast path via global lookup
|
|
132
198
|
let obj = (JOE && JOE.Cache && JOE.Cache.findByID) ? (JOE.Cache.findByID(_id) || null) : null;
|
|
133
|
-
if (!obj &&
|
|
134
|
-
const results = await loadFromStorage(
|
|
199
|
+
if (!obj && itemtype) {
|
|
200
|
+
const results = await loadFromStorage(itemtype, { _id });
|
|
135
201
|
obj = (results && results[0]) || null;
|
|
136
202
|
}
|
|
137
|
-
if (!obj &&
|
|
138
|
-
obj = JOE.Cache.findByID(
|
|
203
|
+
if (!obj && itemtype && JOE && JOE.Cache && JOE.Cache.findByID) {
|
|
204
|
+
obj = JOE.Cache.findByID(itemtype, _id) || null;
|
|
139
205
|
}
|
|
140
|
-
if (!obj) throw new Error(`Object not found${
|
|
206
|
+
if (!obj) throw new Error(`Object not found${itemtype?(' in '+itemtype):''} with _id: ${_id}`);
|
|
141
207
|
if (flatten && JOE && JOE.Utils && JOE.Utils.flattenObject) {
|
|
142
208
|
try { return sanitizeItems(JOE.Utils.flattenObject(_id, { recursive: true, depth }))[0]; } catch(e) {}
|
|
143
209
|
}
|
|
144
210
|
return sanitizeItems(obj)[0];
|
|
145
211
|
},
|
|
146
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
|
+
|
|
147
363
|
/* Deprecated: use unified 'search' instead
|
|
148
364
|
getObjectsByIds: async () => { throw new Error('Use search with ids instead'); },
|
|
149
365
|
queryObjects: async () => { throw new Error('Use search instead'); },
|
|
@@ -153,23 +369,25 @@ MCP.tools = {
|
|
|
153
369
|
searchCache: async () => { throw new Error('Use search instead'); },
|
|
154
370
|
*/
|
|
155
371
|
|
|
156
|
-
// Unified search: defaults to cache; set source="storage" to query DB for a
|
|
157
|
-
|
|
372
|
+
// Unified search: defaults to cache; set source="storage" to query DB for a given itemtype
|
|
373
|
+
// Accepts legacy alias 'schema' for itemtype.
|
|
374
|
+
search: async ({ itemtype, schema, query = {}, ids, source = 'cache', limit = 50, offset = 0, flatten = false, depth = 1, countOnly = false, withCount = false, sortBy, sortDir = 'desc', slim = false }, _ctx) => {
|
|
375
|
+
itemtype = itemtype || schema; // legacy alias
|
|
158
376
|
const useCache = !source || source === 'cache';
|
|
159
377
|
const useStorage = source === 'storage';
|
|
160
378
|
|
|
161
379
|
if (ids && !Array.isArray(ids)) throw new Error("'ids' must be an array if provided");
|
|
162
380
|
|
|
163
|
-
// When ids are provided and
|
|
164
|
-
if (Array.isArray(ids) &&
|
|
381
|
+
// When ids are provided and an itemtype is known, prefer cache for safety/speed
|
|
382
|
+
if (Array.isArray(ids) && itemtype) {
|
|
165
383
|
let items = [];
|
|
166
384
|
if (JOE && JOE.Cache && JOE.Cache.findByID) {
|
|
167
|
-
const found = JOE.Cache.findByID(
|
|
385
|
+
const found = JOE.Cache.findByID(itemtype, ids.join(',')) || [];
|
|
168
386
|
items = Array.isArray(found) ? found : (found ? [found] : []);
|
|
169
387
|
}
|
|
170
388
|
if (useStorage && (!items || items.length === 0)) {
|
|
171
389
|
try {
|
|
172
|
-
const fromStorage = await loadFromStorage(
|
|
390
|
+
const fromStorage = await loadFromStorage(itemtype, { _id: { $in: ids } });
|
|
173
391
|
items = fromStorage || [];
|
|
174
392
|
} catch (e) { /* ignore storage errors here */ }
|
|
175
393
|
}
|
|
@@ -192,7 +410,7 @@ MCP.tools = {
|
|
|
192
410
|
if (useCache) {
|
|
193
411
|
if (!JOE || !JOE.Cache || !JOE.Cache.search) throw new Error('Cache not initialized');
|
|
194
412
|
let results = JOE.Cache.search(query || {});
|
|
195
|
-
if (
|
|
413
|
+
if (itemtype) results = (results || []).filter(i => i && i.itemtype === itemtype);
|
|
196
414
|
results = sortItems(results, sortBy, sortDir);
|
|
197
415
|
if (countOnly) {
|
|
198
416
|
return { count: (results || []).length };
|
|
@@ -206,8 +424,8 @@ MCP.tools = {
|
|
|
206
424
|
}
|
|
207
425
|
|
|
208
426
|
if (useStorage) {
|
|
209
|
-
if (!
|
|
210
|
-
const results = await loadFromStorage(
|
|
427
|
+
if (!itemtype) throw new Error("'itemtype' is required when source=storage");
|
|
428
|
+
const results = await loadFromStorage(itemtype, query || {});
|
|
211
429
|
let sorted = sortItems(results, sortBy, sortDir);
|
|
212
430
|
if (countOnly) {
|
|
213
431
|
return { count: (sorted || []).length };
|
|
@@ -227,7 +445,9 @@ MCP.tools = {
|
|
|
227
445
|
},
|
|
228
446
|
|
|
229
447
|
// Fuzzy, typo-tolerant search over cache with weighted fields
|
|
230
|
-
|
|
448
|
+
// Accepts legacy alias 'schema' for itemtype.
|
|
449
|
+
fuzzySearch: async ({ itemtype, schema, q, filters = {}, fields, threshold = 0.35, limit = 50, offset = 0, highlight = false, minQueryLength = 2 }, _ctx) => {
|
|
450
|
+
itemtype = itemtype || schema; // legacy alias
|
|
231
451
|
if (!q || (q+'').length < (minQueryLength||2)) {
|
|
232
452
|
return { items: [] };
|
|
233
453
|
}
|
|
@@ -235,7 +455,7 @@ MCP.tools = {
|
|
|
235
455
|
var query = Object.assign({}, filters || {});
|
|
236
456
|
query.$fuzzy = { q, fields, threshold, limit, offset, highlight, minQueryLength };
|
|
237
457
|
var results = JOE.Cache.search(query) || [];
|
|
238
|
-
if (
|
|
458
|
+
if (itemtype) { results = results.filter(function(i){ return i && i.itemtype === itemtype; }); }
|
|
239
459
|
var total = (typeof results.count === 'number') ? results.count : results.length;
|
|
240
460
|
if (typeof limit === 'number' && limit > 0) { results = results.slice(0, limit); }
|
|
241
461
|
return { items: sanitizeItems(results), count: total };
|
|
@@ -243,6 +463,8 @@ MCP.tools = {
|
|
|
243
463
|
|
|
244
464
|
// Save an object via Storage (respects events/history)
|
|
245
465
|
saveObject: async ({ object }, ctx = {}) => {
|
|
466
|
+
const Storage = getStorage();
|
|
467
|
+
if (!Storage) throw new Error('Storage module not initialized');
|
|
246
468
|
if (!object || !object.itemtype) throw new Error("'object' with 'itemtype' is required");
|
|
247
469
|
const user = (ctx.req && ctx.req.User) ? ctx.req.User : { name: 'anonymous' };
|
|
248
470
|
// Ensure server-side update timestamp parity with /API/save
|
|
@@ -264,6 +486,8 @@ MCP.tools = {
|
|
|
264
486
|
|
|
265
487
|
// Batch save with bounded concurrency; preserves per-item history/events
|
|
266
488
|
saveObjects: async ({ objects, stopOnError = false, concurrency = 5 } = {}, ctx = {}) => {
|
|
489
|
+
const Storage = getStorage();
|
|
490
|
+
if (!Storage) throw new Error('Storage module not initialized');
|
|
267
491
|
if (!Array.isArray(objects) || objects.length === 0) {
|
|
268
492
|
throw new Error("'objects' (non-empty array) is required");
|
|
269
493
|
}
|
|
@@ -319,6 +543,7 @@ MCP.tools = {
|
|
|
319
543
|
|
|
320
544
|
// Hydration: surface core fields (from file), all schemas, statuses, and tags (no params)
|
|
321
545
|
hydrate: async (_params, _ctx) => {
|
|
546
|
+
var Schemas = getSchemas();
|
|
322
547
|
let coreDef = (JOE && JOE.Fields && JOE.Fields['core']) || null;
|
|
323
548
|
if (!coreDef) {
|
|
324
549
|
try { coreDef = require(__dirname + '/../fields/core.js'); } catch (_e) { coreDef = {}; }
|
|
@@ -372,16 +597,17 @@ MCP.descriptions = {
|
|
|
372
597
|
listSchemas: "List all available JOE schema names.",
|
|
373
598
|
getSchema: "Retrieve schema by name. Set summaryOnly=true for normalized summary instead of full schema.",
|
|
374
599
|
getSchemas: "Retrieve multiple schemas. With summaryOnly=true, returns summaries; if names omitted, returns all.",
|
|
375
|
-
getObject: "Fetch a single object by _id (
|
|
600
|
+
getObject: "Fetch a single object by _id (itemtype optional). Supports optional flatten. Accepts legacy alias 'schema' for itemtype.",
|
|
376
601
|
// getObjectsByIds: "Deprecated - use 'search' with ids.",
|
|
377
602
|
// queryObjects: "Deprecated - use 'search'.",
|
|
378
603
|
// searchCache: "Deprecated - use 'search'.",
|
|
379
|
-
search: "Exact search. Defaults to cache; set source=storage to query DB. Supports sortBy/sortDir, offset/limit paging, withCount, and slim response for {_id,itemtype,name,info,joeUpdated,created}. Use fuzzySearch for typo-tolerant free text.",
|
|
380
|
-
fuzzySearch: "Fuzzy free‑text search. Candidates are prefiltered by 'filters' (e.g., { itemtype: 'user' }) and then scored. If 'fields' are omitted, the schema's searchables (strings) are used with best-field scoring; provide 'fields' (strings or {path,weight}) to use weighted sums. Examples: { q: 'corey hadden', filters: { itemtype: 'user' } } or { q: 'backyard', filters: { itemtype: 'house' } }.",
|
|
604
|
+
search: "Exact search. Defaults to cache; set source=storage to query DB. Supports sortBy/sortDir, offset/limit paging, withCount, and slim response for {_id,itemtype,name,info,joeUpdated,created}. Accepts legacy alias 'schema' for itemtype. Use fuzzySearch for typo-tolerant free text.",
|
|
605
|
+
fuzzySearch: "Fuzzy free‑text search. Candidates are prefiltered by 'filters' (e.g., { itemtype: 'user' }) and then scored. Accepts legacy alias 'schema' for itemtype. If 'fields' are omitted, the schema's searchables (strings) are used with best-field scoring; provide 'fields' (strings or {path,weight}) to use weighted sums. Examples: { q: 'corey hadden', filters: { itemtype: 'user' } } or { q: 'backyard', filters: { itemtype: 'house' } }.",
|
|
381
606
|
saveObject: "Create/update an object; triggers events/history.",
|
|
382
607
|
saveObjects: "Batch save objects with bounded concurrency; per-item history/events preserved.",
|
|
383
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.",
|
|
384
|
-
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."
|
|
385
611
|
};
|
|
386
612
|
|
|
387
613
|
MCP.params = {
|
|
@@ -406,7 +632,7 @@ MCP.params = {
|
|
|
406
632
|
type: "object",
|
|
407
633
|
properties: {
|
|
408
634
|
_id: { type: "string" },
|
|
409
|
-
|
|
635
|
+
itemtype: { type: "string" },
|
|
410
636
|
flatten: { type: "boolean" },
|
|
411
637
|
depth: { type: "integer" }
|
|
412
638
|
},
|
|
@@ -418,7 +644,7 @@ MCP.params = {
|
|
|
418
644
|
search: {
|
|
419
645
|
type: "object",
|
|
420
646
|
properties: {
|
|
421
|
-
|
|
647
|
+
itemtype: { type: "string" },
|
|
422
648
|
query: { type: "object" },
|
|
423
649
|
ids: { type: "array", items: { type: "string" } },
|
|
424
650
|
source: { type: "string", enum: ["cache","storage"] },
|
|
@@ -434,10 +660,21 @@ MCP.params = {
|
|
|
434
660
|
},
|
|
435
661
|
required: []
|
|
436
662
|
},
|
|
437
|
-
|
|
663
|
+
understandObject: {
|
|
438
664
|
type: "object",
|
|
439
665
|
properties: {
|
|
666
|
+
_id: { type: "string" },
|
|
667
|
+
itemtype: { type: "string" },
|
|
440
668
|
schema: { type: "string" },
|
|
669
|
+
depth: { type: "integer" },
|
|
670
|
+
slim: { type: "boolean" }
|
|
671
|
+
},
|
|
672
|
+
required: ["_id"]
|
|
673
|
+
},
|
|
674
|
+
fuzzySearch: {
|
|
675
|
+
type: "object",
|
|
676
|
+
properties: {
|
|
677
|
+
itemtype: { type: "string" },
|
|
441
678
|
q: { type: "string" },
|
|
442
679
|
filters: { type: "object" },
|
|
443
680
|
fields: {
|