claude-home 1.2.2 → 1.4.2
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/bin/cli.js +2 -4
- package/package.json +1 -1
- package/public/index.html +81 -29
- package/server.js +27 -6
package/bin/cli.js
CHANGED
|
@@ -66,10 +66,8 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
66
66
|
if (subcommand === 'stop') {
|
|
67
67
|
const { execSync } = require('child_process');
|
|
68
68
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
execSync(`kill ${pid}`);
|
|
72
|
-
console.log(`✓ claude-home stopped (pid ${pid})`);
|
|
69
|
+
execSync(`lsof -ti:${port} | xargs kill -9`, { stdio: 'ignore' });
|
|
70
|
+
console.log(`✓ claude-home stopped`);
|
|
73
71
|
} catch { console.log(`No claude-home process found on port ${port}`); }
|
|
74
72
|
process.exit(0);
|
|
75
73
|
}
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -343,6 +343,9 @@
|
|
|
343
343
|
|
|
344
344
|
.session-resume-btn:hover { background: var(--red); }
|
|
345
345
|
.session-resume-btn.copied { background: var(--green) !important; }
|
|
346
|
+
.session-resume-btn:disabled { opacity: 0.35; cursor: not-allowed; background: var(--surface-2); color: var(--ink-3); }
|
|
347
|
+
.session-resume-btn:disabled:hover { background: var(--surface-2); }
|
|
348
|
+
.session-row:hover .session-resume-btn:disabled { opacity: 0.4; }
|
|
346
349
|
|
|
347
350
|
/* ── Chat view ────────────────────────────────────────── */
|
|
348
351
|
.chat-header {
|
|
@@ -397,6 +400,10 @@
|
|
|
397
400
|
|
|
398
401
|
.message { display: flex; flex-direction: column; margin-bottom: 24px; }
|
|
399
402
|
|
|
403
|
+
.message.assistant { flex-direction: row; align-items: flex-start; gap: 10px; }
|
|
404
|
+
.message.assistant .message-role { margin-bottom: 0; flex-shrink: 0; padding-top: 3px; }
|
|
405
|
+
.message.assistant .message-bubble .md-content > *:first-child { margin-top: 0; }
|
|
406
|
+
|
|
400
407
|
.message-role {
|
|
401
408
|
font-size: 10px;
|
|
402
409
|
font-weight: 700;
|
|
@@ -431,7 +438,10 @@
|
|
|
431
438
|
align-items: center;
|
|
432
439
|
gap: 8px;
|
|
433
440
|
margin-top: 6px;
|
|
441
|
+
opacity: 0;
|
|
442
|
+
transition: opacity 0.15s;
|
|
434
443
|
}
|
|
444
|
+
.message:hover .message-footer { opacity: 1; }
|
|
435
445
|
|
|
436
446
|
.msg-meta { font-size: 10.5px; color: var(--ink-3); }
|
|
437
447
|
|
|
@@ -443,13 +453,16 @@
|
|
|
443
453
|
.md-content p { margin: 6px 0; }
|
|
444
454
|
.md-content ul, .md-content ol { padding-left: 20px; margin: 6px 0; }
|
|
445
455
|
.md-content li { margin: 3px 0; }
|
|
446
|
-
.md-content code {
|
|
456
|
+
.md-content code, .inline-code {
|
|
447
457
|
background: var(--canvas);
|
|
448
458
|
border: 1px solid var(--rule);
|
|
449
459
|
padding: 1px 5px;
|
|
460
|
+
border-radius: 3px;
|
|
450
461
|
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
451
462
|
font-size: 12px;
|
|
452
463
|
}
|
|
464
|
+
/* keep inline hljs within the bubble background, don't override with theme's white */
|
|
465
|
+
code.inline-code.hljs { background: var(--canvas); }
|
|
453
466
|
.md-content pre {
|
|
454
467
|
background: var(--white);
|
|
455
468
|
border: 1px solid var(--rule);
|
|
@@ -1690,7 +1703,7 @@
|
|
|
1690
1703
|
</template>
|
|
1691
1704
|
<span x-text="formatDate(sessions[0].modified)"></span>
|
|
1692
1705
|
<span x-text="sessions[0].messageCount + ' msgs'"></span>
|
|
1693
|
-
<button class="btn btn-primary btn-sm" style="margin-left:auto" :id="'resume-db'" @click.stop="resumeSession(sessions[0].sessionId,'resume-db')">Resume →</button>
|
|
1706
|
+
<button class="btn btn-primary btn-sm" style="margin-left:auto" :id="'resume-db'" :disabled="!sessions[0].resumable" :title="!sessions[0].resumable ? 'Archivo de sesión eliminado — no se puede retomar' : 'Copiar comando para retomar esta sesión'" @click.stop="sessions[0].resumable && resumeSession(sessions[0].sessionId,'resume-db', sessions[0].projectPath)">Resume →</button>
|
|
1694
1707
|
</div>
|
|
1695
1708
|
</div>
|
|
1696
1709
|
</template>
|
|
@@ -1854,7 +1867,7 @@
|
|
|
1854
1867
|
</div>
|
|
1855
1868
|
</div>
|
|
1856
1869
|
<div class="session-right">
|
|
1857
|
-
<button class="session-resume-btn" :id="'resume-' + s.sessionId" @click.stop="resumeSession(s.sessionId, 'resume-' + s.sessionId)">Resume →</button>
|
|
1870
|
+
<button class="session-resume-btn" :id="'resume-' + s.sessionId" :disabled="!s.resumable" :title="!s.resumable ? 'Archivo de sesión eliminado — no se puede retomar' : 'Copiar comando para retomar esta sesión'" @click.stop="s.resumable && resumeSession(s.sessionId, 'resume-' + s.sessionId, s.projectPath)">Resume →</button>
|
|
1858
1871
|
</div>
|
|
1859
1872
|
</div>
|
|
1860
1873
|
</template>
|
|
@@ -1959,7 +1972,7 @@
|
|
|
1959
1972
|
</div>
|
|
1960
1973
|
</div>
|
|
1961
1974
|
<div style="display:flex;gap:6px;align-items:center;flex-shrink:0">
|
|
1962
|
-
<button class="btn btn-sm" :class="cleanMode ? 'btn-primary' : ''" style="background:var(--canvas-2);color:var(--ink)" :style="cleanMode ? 'background:var(--blue);color:#fff' : ''" @click="cleanMode=!cleanMode" title="Toggle
|
|
1975
|
+
<button class="btn btn-sm" :class="cleanMode ? 'btn-primary' : ''" style="background:var(--canvas-2);color:var(--ink)" :style="cleanMode ? 'background:var(--blue);color:#fff' : ''" @click="cleanMode=!cleanMode" title="Toggle focus view">Focus</button>
|
|
1963
1976
|
<div style="position:relative">
|
|
1964
1977
|
<button class="btn btn-sm" style="background:var(--canvas-2);color:var(--ink);display:flex;align-items:center;gap:4px" @click="exportDropOpen=!exportDropOpen" @click.outside="exportDropOpen=false">
|
|
1965
1978
|
Export <svg width="10" height="10" viewBox="0 0 10 10" fill="currentColor"><path d="M2 3.5L5 6.5L8 3.5"/></svg>
|
|
@@ -1971,7 +1984,7 @@
|
|
|
1971
1984
|
</div>
|
|
1972
1985
|
<button class="btn btn-sm" style="background:var(--red-dim,#3a1a1a);color:var(--red);border:1px solid var(--red)" @click="deleteSession()" x-show="!deletingSession">Delete</button>
|
|
1973
1986
|
<span x-show="deletingSession" style="font-size:12px;color:var(--ink-3)">Deleting…</span>
|
|
1974
|
-
<button class="btn btn-primary btn-sm" id="resume-detail" @click="resumeSession(selectedSession.sessionId, 'resume-detail')">
|
|
1987
|
+
<button class="btn btn-primary btn-sm" id="resume-detail" :disabled="!sessionDetail?.resumable" :title="!sessionDetail?.resumable ? 'Archivo de sesión eliminado — no se puede retomar' : 'Copiar comando para retomar esta sesión'" @click="sessionDetail?.resumable && resumeSession(selectedSession.sessionId, 'resume-detail', selectedSession.projectPath)">
|
|
1975
1988
|
Resume →
|
|
1976
1989
|
</button>
|
|
1977
1990
|
<span x-show="exportMsg" x-text="exportMsg" style="font-size:12px;color:var(--green)" x-transition></span>
|
|
@@ -1992,8 +2005,11 @@
|
|
|
1992
2005
|
<div class="message-bubble">
|
|
1993
2006
|
<template x-for="(block, bi) in getUserBlocks(msg)" :key="bi">
|
|
1994
2007
|
<div>
|
|
2008
|
+
<template x-if="block.type === 'slash-command'">
|
|
2009
|
+
<span style="display:inline-flex;align-items:center;gap:4px;font-size:11px;padding:2px 8px;border-radius:4px;background:var(--canvas-2);border:1px solid var(--rule-2);color:var(--ink-2);font-family:monospace" x-text="block.command"></span>
|
|
2010
|
+
</template>
|
|
1995
2011
|
<template x-if="block.type === 'text'">
|
|
1996
|
-
<div x-
|
|
2012
|
+
<div x-html="renderInlineCode(block.text)" style="white-space:pre-wrap"></div>
|
|
1997
2013
|
</template>
|
|
1998
2014
|
<template x-if="block.type === 'tool_result' && !cleanMode">
|
|
1999
2015
|
<div class="collapsible" style="margin-top:6px">
|
|
@@ -2022,7 +2038,9 @@
|
|
|
2022
2038
|
<!-- Assistant message -->
|
|
2023
2039
|
<template x-if="msg.type === 'assistant'">
|
|
2024
2040
|
<div class="message assistant" x-show="!cleanMode || msgHasText(msg)">
|
|
2025
|
-
<div class="message-role">
|
|
2041
|
+
<div class="message-role">
|
|
2042
|
+
<svg fill="currentColor" fill-rule="evenodd" width="16" height="16" viewBox="0 0 24 24" style="color:var(--ink-3)" xmlns="http://www.w3.org/2000/svg"><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"></path></svg>
|
|
2043
|
+
</div>
|
|
2026
2044
|
<div class="message-bubble">
|
|
2027
2045
|
<template x-for="(block, bi) in getAssistantBlocks(msg)" :key="bi">
|
|
2028
2046
|
<div>
|
|
@@ -3623,8 +3641,10 @@
|
|
|
3623
3641
|
<input class="perm-add-input" style="width:100%" x-model="ruleBuilder.specifier"
|
|
3624
3642
|
placeholder="npm run * / git commit * / * --help *"
|
|
3625
3643
|
@keydown.enter="ruleBuilderAdd()" />
|
|
3626
|
-
<div style="font-size:11px;color:var(--ink-3);margin-top:5px">
|
|
3627
|
-
<code>Bash(ls *)</code> coincide con <code>ls -la</code> pero no con <code>lsof</code> — el espacio antes de <code>*</code> actúa como límite de palabra
|
|
3644
|
+
<div style="font-size:11px;color:var(--ink-3);margin-top:5px;display:flex;flex-direction:column;gap:4px">
|
|
3645
|
+
<span><code>Bash(ls *)</code> coincide con <code>ls -la</code> pero no con <code>lsof</code> — el espacio antes de <code>*</code> actúa como límite de palabra.</span>
|
|
3646
|
+
<span>⚠️ Comandos con <code>&&</code>, <code>|</code> o redirecciones pueden ser bloqueados por la detección automática de patrones peligrosos, aunque estén en allow.</span>
|
|
3647
|
+
<span>💡 Para comandos de una sola herramienta usa el prefijo sin encadenar: <code>npm publish *</code> en lugar de <code>cd /dir && npm publish</code>.</span>
|
|
3628
3648
|
</div>
|
|
3629
3649
|
</div>
|
|
3630
3650
|
</template>
|
|
@@ -3811,14 +3831,26 @@
|
|
|
3811
3831
|
|
|
3812
3832
|
<script>
|
|
3813
3833
|
marked.setOptions({ gfm: true, breaks: true });
|
|
3814
|
-
marked.use({ renderer: {
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3834
|
+
marked.use({ renderer: {
|
|
3835
|
+
code(token) {
|
|
3836
|
+
const lang = token.lang || '';
|
|
3837
|
+
const valid = hljs.getLanguage(lang) ? lang : 'plaintext';
|
|
3838
|
+
try {
|
|
3839
|
+
const highlighted = hljs.highlight(token.text, { language: valid }).value;
|
|
3840
|
+
return `<pre><code class="hljs language-${valid}">${highlighted}</code></pre>`;
|
|
3841
|
+
} catch { return `<pre><code>${token.text}</code></pre>`; }
|
|
3842
|
+
},
|
|
3843
|
+
codespan(token) {
|
|
3844
|
+
try {
|
|
3845
|
+
const highlighted = hljs.highlightAuto(token.text).value;
|
|
3846
|
+
return `<code class="inline-code hljs">${highlighted}</code>`;
|
|
3847
|
+
} catch { return `<code class="inline-code">${token.text}</code>`; }
|
|
3848
|
+
}
|
|
3849
|
+
}});
|
|
3850
|
+
|
|
3851
|
+
function hljsInline(text) {
|
|
3852
|
+
try { return hljs.highlightAuto(text).value; } catch { return text; }
|
|
3853
|
+
}
|
|
3822
3854
|
|
|
3823
3855
|
function app() {
|
|
3824
3856
|
return {
|
|
@@ -3854,7 +3886,7 @@
|
|
|
3854
3886
|
deletingSession: false,
|
|
3855
3887
|
exportDropOpen: false,
|
|
3856
3888
|
exportMsg: '',
|
|
3857
|
-
cleanMode:
|
|
3889
|
+
cleanMode: true,
|
|
3858
3890
|
planExportMsg: '',
|
|
3859
3891
|
planExportOpen: false,
|
|
3860
3892
|
sidebarW: parseInt(localStorage.getItem('cm:sidebarW') || '260'),
|
|
@@ -4045,15 +4077,21 @@
|
|
|
4045
4077
|
await this.openSession(s);
|
|
4046
4078
|
},
|
|
4047
4079
|
|
|
4048
|
-
async resumeSession(sessionId, btnId) {
|
|
4049
|
-
const cmd =
|
|
4050
|
-
|
|
4080
|
+
async resumeSession(sessionId, btnId, projectPath) {
|
|
4081
|
+
const cmd = projectPath
|
|
4082
|
+
? `cd "${projectPath}" && claude -r ${sessionId}`
|
|
4083
|
+
: `claude -r ${sessionId}`;
|
|
4051
4084
|
const btn = document.getElementById(btnId);
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
btn
|
|
4055
|
-
|
|
4056
|
-
|
|
4085
|
+
try {
|
|
4086
|
+
await navigator.clipboard.writeText(cmd);
|
|
4087
|
+
if (btn) {
|
|
4088
|
+
const orig = btn.textContent;
|
|
4089
|
+
btn.textContent = 'Copied!';
|
|
4090
|
+
btn.classList.add('btn-copied', 'copied');
|
|
4091
|
+
setTimeout(() => { btn.textContent = orig; btn.classList.remove('btn-copied', 'copied'); }, 2000);
|
|
4092
|
+
}
|
|
4093
|
+
} catch {
|
|
4094
|
+
prompt('Copia este comando:', cmd);
|
|
4057
4095
|
}
|
|
4058
4096
|
},
|
|
4059
4097
|
|
|
@@ -4253,9 +4291,13 @@
|
|
|
4253
4291
|
|
|
4254
4292
|
getUserBlocks(msg) {
|
|
4255
4293
|
const content = msg.message?.content;
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4294
|
+
const blocks = typeof content === 'string' ? [{ type: 'text', text: content }] : Array.isArray(content) ? content : [];
|
|
4295
|
+
return blocks.map(b => {
|
|
4296
|
+
if (b.type !== 'text') return b;
|
|
4297
|
+
const m = b.text?.match(/<command-name>([^<]+)<\/command-name>/);
|
|
4298
|
+
if (m) return { type: 'slash-command', command: m[1].startsWith('/') ? m[1] : '/' + m[1] };
|
|
4299
|
+
return b;
|
|
4300
|
+
});
|
|
4259
4301
|
},
|
|
4260
4302
|
|
|
4261
4303
|
getAssistantBlocks(msg) {
|
|
@@ -4295,6 +4337,16 @@
|
|
|
4295
4337
|
|
|
4296
4338
|
renderMd(text) { try { return marked.parse(text); } catch { return text; } },
|
|
4297
4339
|
|
|
4340
|
+
renderInlineCode(text) {
|
|
4341
|
+
const esc = text
|
|
4342
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
4343
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
4344
|
+
return esc.replace(/`([^`\n]+)`/g, (_, code) => {
|
|
4345
|
+
const decoded = code.replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&').replace(/"/g,'"');
|
|
4346
|
+
return `<code class="inline-code hljs">${hljsInline(decoded)}</code>`;
|
|
4347
|
+
});
|
|
4348
|
+
},
|
|
4349
|
+
|
|
4298
4350
|
shortProjectName(p) {
|
|
4299
4351
|
if (!p) return 'unknown';
|
|
4300
4352
|
const parts = p.replace(/\\/g, '/').split('/');
|
package/server.js
CHANGED
|
@@ -200,10 +200,15 @@ async function readFirstMessage(filePath) {
|
|
|
200
200
|
if (!timestamp && obj.timestamp) timestamp = obj.timestamp;
|
|
201
201
|
if (!firstPrompt && obj.type === 'user') {
|
|
202
202
|
const content = obj.message?.content;
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
const isHook = typeof content === 'string'
|
|
204
|
+
? content.includes('<local-command-caveat>')
|
|
205
|
+
: Array.isArray(content) && content.every(c => c.type === 'text' && c.text?.includes('<local-command-caveat>'));
|
|
206
|
+
if (!isHook) {
|
|
207
|
+
if (typeof content === 'string' && !content.includes('<command-name>')) firstPrompt = content.slice(0, 300);
|
|
208
|
+
else if (Array.isArray(content)) {
|
|
209
|
+
const txt = content.find(c => c.type === 'text' && !c.text?.includes('<command-name>') && !c.text?.includes('<local-command-caveat>'));
|
|
210
|
+
if (txt) firstPrompt = txt.text.slice(0, 300);
|
|
211
|
+
}
|
|
207
212
|
}
|
|
208
213
|
}
|
|
209
214
|
if (firstPrompt && gitBranch && count > 5) break;
|
|
@@ -267,7 +272,13 @@ async function loadSessionIndex(dirName) {
|
|
|
267
272
|
};
|
|
268
273
|
}));
|
|
269
274
|
|
|
270
|
-
const allEntries = [
|
|
275
|
+
const allEntries = [
|
|
276
|
+
...indexedEntries.map(e => {
|
|
277
|
+
const exists = fs.existsSync(e.fullPath || path.join(dir, `${e.sessionId}.jsonl`));
|
|
278
|
+
return { ...e, orphaned: !exists, resumable: exists };
|
|
279
|
+
}),
|
|
280
|
+
...unindexedEntries.map(e => ({ ...e, resumable: true })),
|
|
281
|
+
];
|
|
271
282
|
indexCache.set(dirName, { indexMtime, dirMtime: dirStat, entries: allEntries });
|
|
272
283
|
return allEntries;
|
|
273
284
|
}
|
|
@@ -291,6 +302,15 @@ async function parseJsonl(filePath, { includeNoise = false, searchText = null }
|
|
|
291
302
|
try { obj = JSON.parse(line); } catch { continue; }
|
|
292
303
|
if (!includeNoise && NOISE_TYPES.has(obj.type)) continue;
|
|
293
304
|
|
|
305
|
+
// Filter out hook output messages (local-command-caveat)
|
|
306
|
+
if (obj.type === 'user') {
|
|
307
|
+
const c = obj.message?.content;
|
|
308
|
+
const isHookMsg = typeof c === 'string'
|
|
309
|
+
? c.includes('<local-command-caveat>')
|
|
310
|
+
: Array.isArray(c) && c.every(b => b.type === 'text' && b.text?.includes('<local-command-caveat>'));
|
|
311
|
+
if (isHookMsg) continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
294
314
|
if (searchText) {
|
|
295
315
|
const text = extractText(obj);
|
|
296
316
|
if (!text.toLowerCase().includes(searchText.toLowerCase())) continue;
|
|
@@ -424,6 +444,7 @@ app.get('/api/sessions/:project/:sessionId', async (req, res) => {
|
|
|
424
444
|
const { project, sessionId } = req.params;
|
|
425
445
|
const filePath = path.join(PROJECTS_DIR, project, `${sessionId}.jsonl`);
|
|
426
446
|
try {
|
|
447
|
+
const resumable = fs.existsSync(filePath);
|
|
427
448
|
const messages = await parseJsonl(filePath);
|
|
428
449
|
const tokens = aggregateTokens(messages);
|
|
429
450
|
const models = [...new Set(
|
|
@@ -435,7 +456,7 @@ app.get('/api/sessions/:project/:sessionId', async (req, res) => {
|
|
|
435
456
|
const cost = calculateCost(tokens, primaryModel);
|
|
436
457
|
const savings = cacheSavings(tokens, primaryModel);
|
|
437
458
|
const carbon = calculateCarbon(tokens, primaryModel);
|
|
438
|
-
res.json({ sessionId, tokens, models, cost, savings, carbon, messages });
|
|
459
|
+
res.json({ sessionId, tokens, models, cost, savings, carbon, messages, resumable });
|
|
439
460
|
} catch (e) {
|
|
440
461
|
res.status(500).json({ error: e.message });
|
|
441
462
|
}
|