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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-object-editor",
3
- "version": "0.10.624",
3
+ "version": "0.10.632",
4
4
  "description": "JOE the Json Object Editor | Platform Edition",
5
5
  "main": "app.js",
6
6
  "scripts": {
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, schema?)` (supports optional `flatten` and `depth`)
47
+ - `getObject(_id, itemtype?)` (supports optional `flatten` and `depth`)
48
48
  - `search` (exact): unified tool for cache and storage
49
- - Params: `{ schema?, query?, ids?, source?: 'cache'|'storage', limit?, flatten?, depth? }`
50
- - Defaults to cache across all collections; add `schema` to filter; set `source:"storage"` to query a specific schema in the DB. Use `fuzzySearch` for typo-tolerant free text.
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: `{ schema?, q, filters?, fields?, threshold?, limit?, offset?, highlight?, minQueryLength? }`
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=@{ schema="<schemaName>"; q="st paal"; threshold=0.35; limit=10 } } | ConvertTo-Json -Depth 10
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=@{ schema="<schemaName>"; source="storage"; query=@{ }; limit=10 } } | ConvertTo-Json -Depth 10
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=@{ schema="<schemaName>"; ids=@("<id1>","<id2>"); flatten=$true; depth=2 } } | ConvertTo-Json -Depth 10
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":{"schema":"<schemaName>","q":"st paal","threshold":0.35,"limit":10}}' | jq
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":{"schema":"<schemaName>","source":"storage","query":{},"limit":10}}' | jq
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
+
@@ -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
 
@@ -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 (schema optional). Prefer cache; fallback to storage.
129
- getObject: async ({ _id, schema, flatten = false, depth = 1 }, _ctx) => {
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 && schema) {
134
- const results = await loadFromStorage(schema, { _id });
199
+ if (!obj && itemtype) {
200
+ const results = await loadFromStorage(itemtype, { _id });
135
201
  obj = (results && results[0]) || null;
136
202
  }
137
- if (!obj && schema && JOE && JOE.Cache && JOE.Cache.findByID) {
138
- obj = JOE.Cache.findByID(schema, _id) || null;
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${schema?(' in '+schema):''} with _id: ${_id}`);
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 schema
157
- search: async ({ schema, query = {}, ids, source = 'cache', limit = 50, offset = 0, flatten = false, depth = 1, countOnly = false, withCount = false, sortBy, sortDir = 'desc', slim = false }, _ctx) => {
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 a schema is known, prefer cache for safety/speed
164
- if (Array.isArray(ids) && schema) {
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(schema, ids.join(',')) || [];
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(schema, { _id: { $in: ids } });
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 (schema) results = (results || []).filter(i => i && i.itemtype === schema);
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 (!schema) throw new Error("'schema' is required when source=storage");
210
- const results = await loadFromStorage(schema, query || {});
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
- fuzzySearch: async ({ schema, q, filters = {}, fields, threshold = 0.35, limit = 50, offset = 0, highlight = false, minQueryLength = 2 }, _ctx) => {
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 (schema) { results = results.filter(function(i){ return i && i.itemtype === schema; }); }
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 (schema optional). Supports optional flatten.",
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
- schema: { type: "string" },
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
- schema: { type: "string" },
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
- fuzzySearch: {
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: {