@mmmbuto/nexuscli 0.5.0
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/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/nexuscli.js +117 -0
- package/frontend/dist/apple-touch-icon.png +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/frontend/dist/assets/index-Bn_l1e6e.css +1 -0
- package/frontend/dist/assets/index-CikJbUR5.js +8617 -0
- package/frontend/dist/browserconfig.xml +12 -0
- package/frontend/dist/favicon-16x16.png +0 -0
- package/frontend/dist/favicon-32x32.png +0 -0
- package/frontend/dist/favicon-48x48.png +0 -0
- package/frontend/dist/favicon.ico +0 -0
- package/frontend/dist/icon-192.png +0 -0
- package/frontend/dist/icon-512.png +0 -0
- package/frontend/dist/icon-maskable-192.png +0 -0
- package/frontend/dist/icon-maskable-512.png +0 -0
- package/frontend/dist/index.html +79 -0
- package/frontend/dist/manifest.json +75 -0
- package/frontend/dist/sw.js +122 -0
- package/frontend/package.json +28 -0
- package/lib/cli/api.js +156 -0
- package/lib/cli/boot.js +172 -0
- package/lib/cli/config.js +185 -0
- package/lib/cli/engines.js +257 -0
- package/lib/cli/init.js +660 -0
- package/lib/cli/logs.js +72 -0
- package/lib/cli/start.js +220 -0
- package/lib/cli/status.js +187 -0
- package/lib/cli/stop.js +64 -0
- package/lib/cli/uninstall.js +194 -0
- package/lib/cli/users.js +295 -0
- package/lib/cli/workspaces.js +337 -0
- package/lib/config/manager.js +233 -0
- package/lib/server/.env.example +20 -0
- package/lib/server/db/adapter.js +314 -0
- package/lib/server/db/drivers/better-sqlite3.js +38 -0
- package/lib/server/db/drivers/sql-js.js +75 -0
- package/lib/server/db/migrate.js +174 -0
- package/lib/server/db/migrations/001_ultra_light_schema.sql +96 -0
- package/lib/server/db/migrations/002_session_conversation_mapping.sql +19 -0
- package/lib/server/db/migrations/003_message_engine_tracking.sql +18 -0
- package/lib/server/db/migrations/004_performance_indexes.sql +16 -0
- package/lib/server/db.js +2 -0
- package/lib/server/lib/cli-wrapper.js +164 -0
- package/lib/server/lib/output-parser.js +132 -0
- package/lib/server/lib/pty-adapter.js +57 -0
- package/lib/server/middleware/auth.js +103 -0
- package/lib/server/models/Conversation.js +259 -0
- package/lib/server/models/Message.js +228 -0
- package/lib/server/models/User.js +115 -0
- package/lib/server/package-lock.json +5895 -0
- package/lib/server/routes/auth.js +168 -0
- package/lib/server/routes/chat.js +206 -0
- package/lib/server/routes/codex.js +205 -0
- package/lib/server/routes/conversations.js +224 -0
- package/lib/server/routes/gemini.js +228 -0
- package/lib/server/routes/jobs.js +317 -0
- package/lib/server/routes/messages.js +60 -0
- package/lib/server/routes/models.js +198 -0
- package/lib/server/routes/sessions.js +285 -0
- package/lib/server/routes/upload.js +134 -0
- package/lib/server/routes/wake-lock.js +95 -0
- package/lib/server/routes/workspace.js +80 -0
- package/lib/server/routes/workspaces.js +142 -0
- package/lib/server/scripts/cleanup-ghost-sessions.js +71 -0
- package/lib/server/scripts/seed-users.js +37 -0
- package/lib/server/scripts/test-history-access.js +50 -0
- package/lib/server/server.js +227 -0
- package/lib/server/services/cache.js +85 -0
- package/lib/server/services/claude-wrapper.js +312 -0
- package/lib/server/services/cli-loader.js +384 -0
- package/lib/server/services/codex-output-parser.js +277 -0
- package/lib/server/services/codex-wrapper.js +224 -0
- package/lib/server/services/context-bridge.js +289 -0
- package/lib/server/services/gemini-output-parser.js +398 -0
- package/lib/server/services/gemini-wrapper.js +249 -0
- package/lib/server/services/history-sync.js +407 -0
- package/lib/server/services/output-parser.js +415 -0
- package/lib/server/services/session-manager.js +465 -0
- package/lib/server/services/summary-generator.js +259 -0
- package/lib/server/services/workspace-manager.js +516 -0
- package/lib/server/tests/history-sync.test.js +90 -0
- package/lib/server/tests/integration-session-sync.test.js +151 -0
- package/lib/server/tests/integration.test.js +76 -0
- package/lib/server/tests/performance.test.js +118 -0
- package/lib/server/tests/services.test.js +160 -0
- package/lib/setup/postinstall.js +216 -0
- package/lib/utils/paths.js +107 -0
- package/lib/utils/termux.js +145 -0
- package/package.json +82 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
const ClaudeWrapper = require('./claude-wrapper');
|
|
2
|
+
const { v4: uuidv4 } = require('uuid');
|
|
3
|
+
const { prepare, saveDb } = require('../db');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SummaryGenerator - produce contextual summaries for sessions.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Generate AI titles for new conversations (uses Claude Haiku - fast & cheap)
|
|
10
|
+
* - Generate contextual summaries for sessions
|
|
11
|
+
* - Auto-save summaries to session_summaries table
|
|
12
|
+
* - Support for summary-based context bridging
|
|
13
|
+
*/
|
|
14
|
+
class SummaryGenerator {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.claude = new ClaudeWrapper(options);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate a short title for a new conversation (3-8 words)
|
|
21
|
+
* Called after first AI response, runs in background
|
|
22
|
+
* @param {string} userMessage - First user message
|
|
23
|
+
* @param {string} assistantResponse - First AI response (optional, for context)
|
|
24
|
+
* @returns {Promise<string>} Generated title
|
|
25
|
+
*/
|
|
26
|
+
async generateTitle(userMessage, assistantResponse = '') {
|
|
27
|
+
const startedAt = Date.now();
|
|
28
|
+
|
|
29
|
+
// Keep context brief for fast generation
|
|
30
|
+
const context = assistantResponse
|
|
31
|
+
? `User: ${userMessage.slice(0, 500)}\nAssistant: ${assistantResponse.slice(0, 500)}`
|
|
32
|
+
: `User: ${userMessage.slice(0, 800)}`;
|
|
33
|
+
|
|
34
|
+
const prompt = `Generate a brief title (3-8 words, no quotes) for this conversation:
|
|
35
|
+
|
|
36
|
+
${context}
|
|
37
|
+
|
|
38
|
+
Reply with ONLY the title, nothing else.`;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const result = await this.claude.sendMessage({
|
|
42
|
+
prompt,
|
|
43
|
+
conversationId: uuidv4(), // Ephemeral session
|
|
44
|
+
model: 'haiku',
|
|
45
|
+
onStatus: () => {} // Silence status
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Clean up the response
|
|
49
|
+
let title = result.text.trim()
|
|
50
|
+
.replace(/^["']|["']$/g, '') // Remove surrounding quotes
|
|
51
|
+
.replace(/^Title:\s*/i, '') // Remove "Title:" prefix
|
|
52
|
+
.slice(0, 60); // Max 60 chars
|
|
53
|
+
|
|
54
|
+
console.log(`[SummaryGenerator] Title generated in ${Date.now() - startedAt}ms: ${title}`);
|
|
55
|
+
return title || 'New Chat';
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('[SummaryGenerator] Title generation failed:', error.message);
|
|
58
|
+
// Fallback to truncated first message
|
|
59
|
+
return userMessage.slice(0, 50).trim() + (userMessage.length > 50 ? '...' : '');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Generate or refresh summary for a session.
|
|
65
|
+
* @param {Object} params
|
|
66
|
+
* @param {string} params.sessionId
|
|
67
|
+
* @param {Array} params.messages - Array of {role, content, created_at}
|
|
68
|
+
* @param {Object|null} params.existingSummary
|
|
69
|
+
* @returns {Promise<Object>} summary payload
|
|
70
|
+
*/
|
|
71
|
+
async generateSummary({ sessionId, messages = [], existingSummary = null }) {
|
|
72
|
+
if (!sessionId) throw new Error('sessionId is required');
|
|
73
|
+
|
|
74
|
+
const startedAt = Date.now();
|
|
75
|
+
|
|
76
|
+
// Build transcript snippet (limit to 40 latest messages for brevity)
|
|
77
|
+
const transcript = this.buildTranscript(messages.slice(-40));
|
|
78
|
+
|
|
79
|
+
const prompt = this.buildPrompt({ sessionId, transcript, existingSummary });
|
|
80
|
+
|
|
81
|
+
const result = await this.claude.sendMessage({
|
|
82
|
+
prompt,
|
|
83
|
+
conversationId: uuidv4(), // Ephemeral session (Claude CLI requires valid UUID)
|
|
84
|
+
model: 'haiku',
|
|
85
|
+
onStatus: () => {} // silence status for summaries
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const parsed = this.safeParseJson(result.text);
|
|
89
|
+
|
|
90
|
+
console.log(`[SummaryGenerator] Summary for ${sessionId} took ${Date.now() - startedAt}ms`);
|
|
91
|
+
|
|
92
|
+
return parsed;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Save summary to database
|
|
97
|
+
* @param {string} sessionId
|
|
98
|
+
* @param {Object} summary - {summary_short, summary_long, key_decisions, tools_used, files_modified}
|
|
99
|
+
* @returns {boolean} Success
|
|
100
|
+
*/
|
|
101
|
+
saveSummary(sessionId, summary) {
|
|
102
|
+
try {
|
|
103
|
+
const stmt = prepare(`
|
|
104
|
+
INSERT OR REPLACE INTO session_summaries
|
|
105
|
+
(session_id, summary_short, summary_long, key_decisions, tools_used, files_modified, updated_at, version)
|
|
106
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT version FROM session_summaries WHERE session_id = ?), 0) + 1)
|
|
107
|
+
`);
|
|
108
|
+
|
|
109
|
+
stmt.run(
|
|
110
|
+
sessionId,
|
|
111
|
+
summary.summary_short || '',
|
|
112
|
+
summary.summary_long || null,
|
|
113
|
+
JSON.stringify(summary.key_decisions || []),
|
|
114
|
+
JSON.stringify(summary.tools_used || []),
|
|
115
|
+
JSON.stringify(summary.files_modified || []),
|
|
116
|
+
Date.now(),
|
|
117
|
+
sessionId
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
saveDb();
|
|
121
|
+
console.log(`[SummaryGenerator] Saved summary for ${sessionId}`);
|
|
122
|
+
return true;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(`[SummaryGenerator] Failed to save summary:`, error.message);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get existing summary from database
|
|
131
|
+
* @param {string} sessionId
|
|
132
|
+
* @returns {Object|null}
|
|
133
|
+
*/
|
|
134
|
+
getSummary(sessionId) {
|
|
135
|
+
try {
|
|
136
|
+
const stmt = prepare('SELECT * FROM session_summaries WHERE session_id = ?');
|
|
137
|
+
const row = stmt.get(sessionId);
|
|
138
|
+
|
|
139
|
+
if (!row) return null;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
summary_short: row.summary_short,
|
|
143
|
+
summary_long: row.summary_long,
|
|
144
|
+
key_decisions: JSON.parse(row.key_decisions || '[]'),
|
|
145
|
+
tools_used: JSON.parse(row.tools_used || '[]'),
|
|
146
|
+
files_modified: JSON.parse(row.files_modified || '[]'),
|
|
147
|
+
updated_at: row.updated_at,
|
|
148
|
+
version: row.version
|
|
149
|
+
};
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.warn(`[SummaryGenerator] Failed to get summary:`, error.message);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Generate summary and save to DB (convenience method)
|
|
158
|
+
* @param {string} sessionId
|
|
159
|
+
* @param {Array} messages
|
|
160
|
+
* @returns {Promise<Object|null>}
|
|
161
|
+
*/
|
|
162
|
+
async generateAndSave(sessionId, messages) {
|
|
163
|
+
try {
|
|
164
|
+
const existingSummary = this.getSummary(sessionId);
|
|
165
|
+
const summary = await this.generateSummary({ sessionId, messages, existingSummary });
|
|
166
|
+
this.saveSummary(sessionId, summary);
|
|
167
|
+
return summary;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(`[SummaryGenerator] generateAndSave failed:`, error.message);
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get summary text for context bridging
|
|
176
|
+
* Returns a formatted string suitable for prefixing prompts
|
|
177
|
+
* @param {string} sessionId
|
|
178
|
+
* @returns {string|null}
|
|
179
|
+
*/
|
|
180
|
+
getBridgeContext(sessionId) {
|
|
181
|
+
const summary = this.getSummary(sessionId);
|
|
182
|
+
if (!summary || !summary.summary_short) return null;
|
|
183
|
+
|
|
184
|
+
let context = `[Session Summary]\n${summary.summary_short}`;
|
|
185
|
+
|
|
186
|
+
if (summary.key_decisions && summary.key_decisions.length > 0) {
|
|
187
|
+
context += `\n\nKey decisions:\n- ${summary.key_decisions.slice(0, 5).join('\n- ')}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (summary.files_modified && summary.files_modified.length > 0) {
|
|
191
|
+
context += `\n\nFiles worked on:\n- ${summary.files_modified.slice(0, 10).join('\n- ')}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return context;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
buildTranscript(messages) {
|
|
198
|
+
return messages
|
|
199
|
+
.map(m => {
|
|
200
|
+
const role = m.role || 'assistant';
|
|
201
|
+
const content = (m.content || '').trim();
|
|
202
|
+
const ts = m.created_at ? new Date(m.created_at).toISOString() : '';
|
|
203
|
+
return `[${ts}] ${role.toUpperCase()}: ${content}`;
|
|
204
|
+
})
|
|
205
|
+
.join('\n')
|
|
206
|
+
.slice(-6000); // keep prompt size reasonable
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
buildPrompt({ sessionId, transcript, existingSummary }) {
|
|
210
|
+
const existing = existingSummary
|
|
211
|
+
? `Existing summary (for refresh): ${JSON.stringify(existingSummary)}`
|
|
212
|
+
: 'No existing summary.';
|
|
213
|
+
|
|
214
|
+
return `
|
|
215
|
+
You are a concise assistant. Summarize the coding/chat session into JSON.
|
|
216
|
+
Session ID: ${sessionId}
|
|
217
|
+
|
|
218
|
+
${existing}
|
|
219
|
+
|
|
220
|
+
Provide JSON with keys:
|
|
221
|
+
- summary_short (<=80 words)
|
|
222
|
+
- summary_long (<=200 words)
|
|
223
|
+
- key_decisions (array of short bullet strings)
|
|
224
|
+
- tools_used (array of tool names or commands)
|
|
225
|
+
- files_modified (array of file paths)
|
|
226
|
+
|
|
227
|
+
Do not include any extra text outside valid JSON.
|
|
228
|
+
|
|
229
|
+
Transcript:
|
|
230
|
+
${transcript}
|
|
231
|
+
`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
safeParseJson(text) {
|
|
235
|
+
// Try to extract JSON block
|
|
236
|
+
const start = text.indexOf('{');
|
|
237
|
+
const end = text.lastIndexOf('}');
|
|
238
|
+
if (start === -1 || end === -1) {
|
|
239
|
+
throw new Error('Failed to parse summary JSON');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const jsonString = text.slice(start, end + 1);
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const parsed = JSON.parse(jsonString);
|
|
246
|
+
// Normalize array fields
|
|
247
|
+
['key_decisions', 'tools_used', 'files_modified'].forEach(key => {
|
|
248
|
+
if (parsed[key] && !Array.isArray(parsed[key])) {
|
|
249
|
+
parsed[key] = [parsed[key]].filter(Boolean);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
return parsed;
|
|
253
|
+
} catch (e) {
|
|
254
|
+
throw new Error('Invalid JSON returned by model');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = SummaryGenerator;
|