json-object-editor 0.10.664 → 0.10.665

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,4 +1,10 @@
1
- ## 0.10.663
1
+ ## 0.10.665
2
+
3
+ - AI Response JSON extraction and audit trail improvements
4
+ - **JSON extraction**: Enhanced `extractJsonText` to handle cases where tool call logs and reasoning text are concatenated before the actual JSON response. The function now finds all JSON objects in the response, filters out tool call logs (objects starting with `{"tool":`), and returns the largest valid JSON object that matches the expected response structure.
5
+ - **Raw response preservation**: Added `response_raw` field to `ai_response` schema to preserve the complete original response from OpenAI (including tool logs, reasoning text, etc.) for full audit/debugging while `response` contains the cleaned JSON string for merges.
6
+ - **Merge reliability**: Updated `compareResponseToObject` and `updateObjectFromResponse` methods to prefer `response_json` (already parsed) when available, with fallback to parsing `response` string, reducing double-parsing and improving reliability.
7
+ - **Response storage**: `executeJOEAiPrompt` now stores both `response_raw` (original) and `response` (cleaned JSON) to maintain complete audit trail while ensuring merge operations work with clean, parseable JSON.## 0.10.663
2
8
 
3
9
  - Plugin inventory debug page
4
10
  - 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.
package/css/joe.css CHANGED
@@ -1,6 +1,6 @@
1
1
  /* --------------------------------------------------------
2
2
  *
3
- * json-object-editor - v0.10.664
3
+ * json-object-editor - v0.10.665
4
4
  * Created by: Corey Hadden
5
5
  *
6
6
  * -------------------------------------------------------- */
package/js/joe.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /* --------------------------------------------------------
2
2
  *
3
- * json-object-editor - v0.10.664
3
+ * json-object-editor - v0.10.665
4
4
  * Created by: Corey Hadden
5
5
  *
6
6
  * -------------------------------------------------------- */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-object-editor",
3
- "version": "0.10.664",
3
+ "version": "0.10.665",
4
4
  "description": "JOE the Json Object Editor | Platform Edition",
5
5
  "main": "app.js",
6
6
  "scripts": {
@@ -887,6 +887,7 @@ function shrinkUnderstandObjectMessagesForTokens(messages) {
887
887
  // in markdown fences (```json ... ```), and may prepend/append prose. This
888
888
  // helper strips fences and tries to isolate the first well-formed JSON
889
889
  // object/array substring so JSON.parse has the best chance of succeeding.
890
+ // Handles cases where tool call logs are concatenated before the actual JSON.
890
891
  function extractJsonText(raw) {
891
892
  if (!raw) { return ''; }
892
893
  let t = String(raw).trim();
@@ -906,19 +907,100 @@ function shrinkUnderstandObjectMessagesForTokens(messages) {
906
907
  }
907
908
  t = t.trim();
908
909
  }
909
- // If there's extra prose around the JSON, slice from first {/[ to last }/]
910
+
911
+ // Handle cases where tool call logs (small JSON objects like {"tool":"..."})
912
+ // are concatenated before the actual response JSON (larger JSON object).
913
+ // Find all JSON objects and pick the largest one that's not a tool log.
914
+ const jsonCandidates = [];
915
+ const firstBrace = t.indexOf('{');
916
+ const firstBracket = t.indexOf('[');
917
+ const lastBrace = Math.max(t.lastIndexOf('}'), t.lastIndexOf(']'));
918
+
919
+ if (firstBrace === -1 && firstBracket === -1) {
920
+ return '';
921
+ }
922
+
923
+ const startPos = (firstBrace === -1) ? firstBracket :
924
+ ((firstBracket === -1) ? firstBrace : Math.min(firstBrace, firstBracket));
925
+
926
+ if (startPos === -1 || lastBrace === -1 || lastBrace <= startPos) {
927
+ return t.trim();
928
+ }
929
+
930
+ // Find all potential JSON objects
931
+ for (let i = startPos; i <= lastBrace; i++) {
932
+ if (t[i] !== '{' && t[i] !== '[') continue;
933
+
934
+ // Find matching closing brace/bracket
935
+ let depth = 0;
936
+ let inString = false;
937
+ let escape = false;
938
+ let endPos = -1;
939
+
940
+ for (let j = i; j <= lastBrace; j++) {
941
+ const char = t[j];
942
+ if (escape) {
943
+ escape = false;
944
+ continue;
945
+ }
946
+ if (char === '\\') {
947
+ escape = true;
948
+ continue;
949
+ }
950
+ if (char === '"') {
951
+ inString = !inString;
952
+ continue;
953
+ }
954
+ if (!inString) {
955
+ if (char === '{' || char === '[') {
956
+ depth++;
957
+ } else if (char === '}' || char === ']') {
958
+ depth--;
959
+ if (depth === 0) {
960
+ endPos = j;
961
+ break;
962
+ }
963
+ }
964
+ }
965
+ }
966
+
967
+ if (endPos !== -1) {
968
+ const candidate = t.substring(i, endPos + 1);
969
+ // Skip tool call logs - they match pattern {"tool":"..."}
970
+ const isToolLog = /^\s*{\s*"tool"\s*:/.test(candidate);
971
+ try {
972
+ JSON.parse(candidate);
973
+ jsonCandidates.push({
974
+ text: candidate,
975
+ length: candidate.length,
976
+ isToolLog: isToolLog
977
+ });
978
+ } catch (e) {
979
+ // Not valid JSON, skip
980
+ }
981
+ }
982
+ }
983
+
984
+ // Find the largest non-tool-log JSON object, or largest overall if all are tool logs
985
+ if (jsonCandidates.length > 0) {
986
+ // Filter out tool logs first
987
+ const nonToolLogs = jsonCandidates.filter(c => !c.isToolLog);
988
+ const candidatesToUse = nonToolLogs.length > 0 ? nonToolLogs : jsonCandidates;
989
+
990
+ // Sort by length (descending) and return the largest
991
+ candidatesToUse.sort((a, b) => b.length - a.length);
992
+ return candidatesToUse[0].text.trim();
993
+ }
994
+
995
+ // Fallback: try simple first-to-last extraction
910
996
  if (t[0] !== '{' && t[0] !== '[') {
911
- const firstBrace = t.indexOf('{');
912
- const firstBracket = t.indexOf('[');
913
- let first = -1;
914
- if (firstBrace === -1) { first = firstBracket; }
915
- else if (firstBracket === -1) { first = firstBrace; }
916
- else { first = Math.min(firstBrace, firstBracket); }
917
- const lastBrace = Math.max(t.lastIndexOf('}'), t.lastIndexOf(']'));
918
- if (first !== -1 && lastBrace !== -1 && lastBrace > first) {
919
- t = t.slice(first, lastBrace + 1);
997
+ const first = startPos;
998
+ const last = lastBrace;
999
+ if (first !== -1 && last !== -1 && last > first) {
1000
+ t = t.slice(first, last + 1);
920
1001
  }
921
1002
  }
1003
+
922
1004
  return t.trim();
923
1005
  }
924
1006
 
@@ -1357,9 +1439,27 @@ this.executeJOEAiPrompt = async function(data, req, res) {
1357
1439
 
1358
1440
  // const response = await openai.chat.completions.create(payload);
1359
1441
 
1442
+ // Extract JSON from response to strip tool logs, reasoning text, and other non-JSON content.
1443
+ // This is critical for prompts that explicitly require JSON-only output.
1444
+ const rawResponseText = response.output_text || "";
1445
+ let cleanedResponseText = rawResponseText;
1446
+ try {
1447
+ const extractedJson = extractJsonText(rawResponseText);
1448
+ if (extractedJson && extractedJson.trim().length > 0) {
1449
+ // Validate it's actually valid JSON
1450
+ JSON.parse(extractedJson);
1451
+ cleanedResponseText = extractedJson;
1452
+ }
1453
+ } catch (e) {
1454
+ // If extraction fails or JSON is invalid, fall back to raw text
1455
+ // (some prompts may not be JSON-formatted)
1456
+ console.warn('[chatgpt.executeJOEAiPrompt] Failed to extract JSON from response, using raw text:', e.message);
1457
+ }
1458
+
1360
1459
  const saved = await saveAiResponseRefactor({
1361
1460
  prompt,
1362
- ai_response_content: response.output_text || "",
1461
+ ai_response_content: cleanedResponseText,
1462
+ ai_response_raw: rawResponseText,
1363
1463
  user_prompt: finalInput || '',
1364
1464
  params,
1365
1465
  referenced_object_ids: referencedObjectIds,
@@ -1382,7 +1482,7 @@ this.executeJOEAiPrompt = async function(data, req, res) {
1382
1482
  }
1383
1483
  }catch(_e){}
1384
1484
 
1385
- return { success: true, ai_response_id: saved._id,response:response.output_text || "",usage:response.usage };
1485
+ return { success: true, ai_response_id: saved._id,response:cleanedResponseText,usage:response.usage };
1386
1486
  } catch (e) {
1387
1487
  console.error('❌ executeJOEAiPrompt error:', e);
1388
1488
  return { error: "Failed to execute AI prompt.",message: e.message };
@@ -1402,7 +1502,7 @@ this.executeJOEAiPrompt = async function(data, req, res) {
1402
1502
  max_tokens: prompt.max_tokens ?? 1200
1403
1503
  };
1404
1504
  }
1405
- async function saveAiResponseRefactor({ prompt, ai_response_content, user_prompt, params, referenced_object_ids,response_id,usage,user,ai_assistant_id, mcp_enabled, mcp_toolset, mcp_selected_tools, mcp_instructions_mode, mcp_tools_used }) {
1505
+ async function saveAiResponseRefactor({ prompt, ai_response_content, ai_response_raw, user_prompt, params, referenced_object_ids,response_id,usage,user,ai_assistant_id, mcp_enabled, mcp_toolset, mcp_selected_tools, mcp_instructions_mode, mcp_tools_used }) {
1406
1506
  var response_keys = [];
1407
1507
  try {
1408
1508
  response_keys = Object.keys(JSON.parse(ai_response_content));
@@ -1437,6 +1537,7 @@ this.executeJOEAiPrompt = async function(data, req, res) {
1437
1537
  prompt_name: prompt.name,
1438
1538
  prompt_method:prompt.prompt_method,
1439
1539
  response: ai_response_content,
1540
+ response_raw: ai_response_raw || null,
1440
1541
  response_json: parsedResponse,
1441
1542
  response_keys: response_keys,
1442
1543
  response_id:response_id||'',
@@ -30,6 +30,7 @@ var schema = {
30
30
  { name:'model_used', type:'string' },
31
31
  { name:'response_type', type:'string' },
32
32
  { name:'response', type:'string' },
33
+ { name:'response_raw', type:'string' },
33
34
  { name:'response_json', type:'object' },
34
35
  { name:'response_keys', type:'string', isArray:true },
35
36
  { name:'response_id', type:'string' },