claude-mem-lite 6.8.2 → 6.8.3
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/adopt-cli.mjs +11 -0
- package/claudemd.mjs +95 -7
- package/lib/db-unusable.mjs +43 -0
- package/mem-cli.mjs +7 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server.mjs +12 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.8.
|
|
12
|
+
"version": "6.8.3",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"homepage": "https://github.com/sdsrss/claude-mem-lite",
|
|
15
15
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.8.
|
|
3
|
+
"version": "6.8.3",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/adopt-cli.mjs
CHANGED
|
@@ -425,6 +425,13 @@ function unadoptAll(args) {
|
|
|
425
425
|
log(`[unadopt --all] ${dir} → cleaned partial residue (detail doc/state, no block)`);
|
|
426
426
|
partial++;
|
|
427
427
|
}
|
|
428
|
+
// OUTSIDE the branch, because residue is orthogonal to what happened to the block: a
|
|
429
|
+
// project can have its block removed AND still carry an unpaired sentinel. An orphan is
|
|
430
|
+
// the one kind of residue the sweep cannot finish — its block has no end marker, so its
|
|
431
|
+
// extent is unknowable — and the two lines above would otherwise imply the project is
|
|
432
|
+
// clean. Inside the else-branch it was also unreachable for the 'removed' case, which is
|
|
433
|
+
// how the print survived a mutation with the whole suite green (pre-ship review P2-2).
|
|
434
|
+
if (r.residue) log(` ⚠ ${r.residue}`);
|
|
428
435
|
}
|
|
429
436
|
|
|
430
437
|
// 2. Legacy memory-dir cleanup across every memdir (foreign-content guarded).
|
|
@@ -481,4 +488,8 @@ export function cmdUnadopt(args = []) {
|
|
|
481
488
|
const mig = migrateLegacyMemoryDir(cwd, PLUGIN_SLUG, { force });
|
|
482
489
|
const migNote = mig.action === 'removed' ? ' (+cleaned legacy memdir)' : '';
|
|
483
490
|
log(`[unadopt] ${cwd} → ${r.action}${migNote}`);
|
|
491
|
+
// 'partial' is the outcome that used to print as 'absent': the sidecar files are gone but
|
|
492
|
+
// an unpaired sentinel still holds steering text in the user's CLAUDE.md, and only they can
|
|
493
|
+
// decide where that text ends. Silence here is what let it survive every sweep.
|
|
494
|
+
if (r.residue) log(` ⚠ ${r.residue}`);
|
|
484
495
|
}
|
package/claudemd.mjs
CHANGED
|
@@ -59,8 +59,50 @@ function escapeRe(s) {
|
|
|
59
59
|
// are `\r?\n` (not bare `\n`) so a CLAUDE.md re-saved with Windows CRLF endings
|
|
60
60
|
// still matches — otherwise the block read as "absent" and a fresh LF copy got
|
|
61
61
|
// appended every SessionStart, growing the file without bound (review C1/H2).
|
|
62
|
+
// The body may not contain ANOTHER sentinel of the same slug. `[\s\S]*?` could, and that is
|
|
63
|
+
// not a tidiness point — it is how a match stopped being one block. Drop the `:end` line by
|
|
64
|
+
// hand (a merge resolution, an editor, another tool) and the next adopt appends a second
|
|
65
|
+
// block below whatever the user has written since; the adopt after THAT matched from the
|
|
66
|
+
// orphaned begin, lazily, to the only `:end` in the file — which now sits past the user's
|
|
67
|
+
// text and past the second begin — so `raw.replace(m[0], section)` deleted all of it.
|
|
68
|
+
// Measured 2026-09-13: a "## Deployment runbook" section appended after adoption was gone
|
|
69
|
+
// after two further adopts, silently, with both runs reporting success.
|
|
70
|
+
//
|
|
71
|
+
// The tempered token below cannot span a sentinel, so the engine backtracks to the
|
|
72
|
+
// WELL-FORMED pair and the orphan is simply left alone — which is the right answer for a
|
|
73
|
+
// file we do not own: where the orphaned body ends is genuinely unknowable, so removeManaged
|
|
74
|
+
// reports it (action 'partial') rather than guessing a span to delete.
|
|
75
|
+
//
|
|
76
|
+
// Safe by construction for legitimate blocks: the shipped body carries the slug twice and
|
|
77
|
+
// never as a sentinel (measured: 1304 bytes — 1296 UTF-16 units, the body has em dashes —
|
|
78
|
+
// with zero `:begin` / `:end` occurrences), and the
|
|
79
|
+
// separators stay `\r?\n` for the CRLF reason below.
|
|
62
80
|
function blockBody(esc) {
|
|
63
|
-
|
|
81
|
+
const sentinel = `<!-- ${esc}:(?:begin|end)`;
|
|
82
|
+
return `<!-- ${esc}:begin (v\\d+) -->\\r?\\n((?:(?!${sentinel})[\\s\\S])*?)\\r?\\n<!-- ${esc}:end -->`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Any sentinel LINE of our slug, paired or not. The pair regex above is deliberately blind to
|
|
86
|
+
// an unpaired one; this is what lets residue reporting see what it cannot safely remove.
|
|
87
|
+
function sentinelLineRegexG(slug) {
|
|
88
|
+
return new RegExp(`<!-- ${escapeRe(slug)}:(?:begin|end)\\b[^>]*-->`, 'g');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Sentinel lines of this slug left in `raw` that no well-formed block accounts for.
|
|
93
|
+
* Zero on a healthy file (every sentinel belongs to a matched pair) and on a clean one.
|
|
94
|
+
* @param {string} raw
|
|
95
|
+
* @param {string} slug
|
|
96
|
+
* @returns {number}
|
|
97
|
+
*/
|
|
98
|
+
function orphanSentinelCount(raw, slug) {
|
|
99
|
+
const total = (raw.match(sentinelLineRegexG(slug)) || []).length;
|
|
100
|
+
let paired = 0;
|
|
101
|
+
raw.replace(blockRegexG(slug), (whole) => {
|
|
102
|
+
paired += (whole.match(sentinelLineRegexG(slug)) || []).length;
|
|
103
|
+
return whole;
|
|
104
|
+
});
|
|
105
|
+
return total - paired;
|
|
64
106
|
}
|
|
65
107
|
function blockRegex(slug) {
|
|
66
108
|
return new RegExp(blockBody(escapeRe(slug)));
|
|
@@ -137,13 +179,24 @@ export function isAdopted(cwd, slug) {
|
|
|
137
179
|
* removed it. removeManaged cleans all three pieces, so sweep on any of them.
|
|
138
180
|
*/
|
|
139
181
|
export function hasResidue(cwd, slug) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
existsSync(detailDocPath(cwd, slug)) ||
|
|
143
|
-
existsSync(stateFilePath(cwd, slug))
|
|
144
|
-
);
|
|
182
|
+
const blk = readBlock(cwd, slug);
|
|
183
|
+
return blk.body !== null || existsSync(detailDocPath(cwd, slug)) || existsSync(stateFilePath(cwd, slug));
|
|
145
184
|
}
|
|
146
185
|
|
|
186
|
+
// An unpaired sentinel is deliberately NOT in the list above (pre-ship review P2-3). A first
|
|
187
|
+
// cut added it, reasoning that the sweep should see what the pair regex cannot. But
|
|
188
|
+
// orphanSentinelCount counts sentinel-shaped TEXT, and a project the plugin never touched can
|
|
189
|
+
// mention the marker in prose — documenting it, pasting half an example, a changelog line.
|
|
190
|
+
// That one mention let `unadopt --all` into a stranger's project, where removeManaged
|
|
191
|
+
// unconditionally deletes the detail doc and state sidecar and rmdir's an empty `.claude/`,
|
|
192
|
+
// then printed "remove those lines by hand" at the user's own paragraph — and never
|
|
193
|
+
// converged, because the mention is still there on the next sweep.
|
|
194
|
+
//
|
|
195
|
+
// The three entries above are all things the PLUGIN WROTE; a sentinel in prose is not. And
|
|
196
|
+
// the sweep gains nothing by entering: removeManaged cannot clean an orphan anyway, by
|
|
197
|
+
// design. The orphan is reported by removeManaged when unadopt genuinely runs — which is the
|
|
198
|
+
// real failure case, where the doc and sidecar are still present and do bring it in.
|
|
199
|
+
|
|
147
200
|
/**
|
|
148
201
|
* Whether the installed block/doc has drifted from the shipped content — i.e.
|
|
149
202
|
* a version bump or a template edit means we should refresh. Returns true when
|
|
@@ -232,11 +285,25 @@ export function writeManaged(cwd, { slug, version, block, doc }) {
|
|
|
232
285
|
* Remove our managed block from CLAUDE.md (preserving all other content) and
|
|
233
286
|
* delete the detail doc + state sidecar. Best-effort removes an emptied
|
|
234
287
|
* .claude/ directory.
|
|
235
|
-
*
|
|
288
|
+
*
|
|
289
|
+
* THREE outcomes, not two — the same rule lib/db-unusable.mjs states about backups: "there
|
|
290
|
+
* is nothing to do" and "I could not finish" must not print in the same voice, because a
|
|
291
|
+
* green-sounding line ends the reader's search. `absent` used to cover both: with one
|
|
292
|
+
* sentinel line missing, the pair regex matched nothing, so this returned 'absent' — while
|
|
293
|
+
* having already deleted the detail doc and the state sidecar and left ~1.3 KB of managed
|
|
294
|
+
* steering text in the user's CLAUDE.md, which it is then loaded from on every session.
|
|
295
|
+
* `partial` is that case, and `residue` names what is left so the caller can say so.
|
|
296
|
+
*
|
|
297
|
+
* Deliberately does NOT delete an orphaned sentinel's body: where it ends is unknowable
|
|
298
|
+
* (that is the defect, not a detail), and guessing a span in a file we do not own is how the
|
|
299
|
+
* adopt side came to delete a user's runbook. Report, do not repair.
|
|
300
|
+
*
|
|
301
|
+
* @returns {{action: 'removed'|'partial'|'absent', residue?: string}}
|
|
236
302
|
*/
|
|
237
303
|
export function removeManaged(cwd, slug) {
|
|
238
304
|
const p = claudeMdPath(cwd);
|
|
239
305
|
let action = 'absent';
|
|
306
|
+
let orphans = 0;
|
|
240
307
|
if (existsSync(p)) {
|
|
241
308
|
let raw = readFileSync(p, 'utf8');
|
|
242
309
|
// H2: loop so ALL same-slug blocks are removed, not just the first (a
|
|
@@ -283,8 +350,17 @@ export function removeManaged(cwd, slug) {
|
|
|
283
350
|
atomicWrite(p, raw);
|
|
284
351
|
}
|
|
285
352
|
}
|
|
353
|
+
// Counted on what is left AFTER the loop, so a healthy file (every sentinel consumed by
|
|
354
|
+
// a matched pair) reports zero and only a genuinely unpaired line survives the count.
|
|
355
|
+
orphans = orphanSentinelCount(raw, slug);
|
|
286
356
|
}
|
|
357
|
+
// Captured BEFORE the deletions below, because they are what it asks about: is there any
|
|
358
|
+
// evidence the plugin ever wrote in this project? An unpaired sentinel is NOT such
|
|
359
|
+
// evidence — it is text, and a project that merely documents the marker in prose has one
|
|
360
|
+
// (pre-ship review P2-3). Reporting residue there means telling a stranger to delete their
|
|
361
|
+
// own paragraph, on a project this tool has never touched.
|
|
287
362
|
const dp = detailDocPath(cwd, slug);
|
|
363
|
+
const wasOurs = action === 'removed' || existsSync(dp) || existsSync(stateFilePath(cwd, slug));
|
|
288
364
|
if (existsSync(dp))
|
|
289
365
|
try {
|
|
290
366
|
unlinkSync(dp);
|
|
@@ -300,6 +376,18 @@ export function removeManaged(cwd, slug) {
|
|
|
300
376
|
} catch {
|
|
301
377
|
/* best-effort */
|
|
302
378
|
}
|
|
379
|
+
// `action` answers ONE question — what happened to the block — and `residue` is an
|
|
380
|
+
// independent fact that rides alongside it. A first cut let an orphan override 'removed'
|
|
381
|
+
// too, on the reasoning that both are "unfinished". Pre-ship review P2-1: unadoptAll's
|
|
382
|
+
// else-branch prints "cleaned partial residue (detail doc/state, no block)" and counts
|
|
383
|
+
// `partial++`, so a sweep that DID remove a block reported "no block" and tallied zero
|
|
384
|
+
// removals. Two facts, two fields.
|
|
385
|
+
const residue =
|
|
386
|
+
orphans > 0 && wasOurs
|
|
387
|
+
? `${orphans} unpaired \`${slug}\` sentinel line(s) remain in ${claudeMdPath(cwd)} — the block they opened has no matching end marker, so its extent cannot be determined safely. Remove those lines and the text they wrap by hand.`
|
|
388
|
+
: null;
|
|
389
|
+
if (action === 'removed') return residue ? { action, residue } : { action };
|
|
390
|
+
if (residue) return { action: 'partial', residue };
|
|
303
391
|
return { action };
|
|
304
392
|
}
|
|
305
393
|
|
package/lib/db-unusable.mjs
CHANGED
|
@@ -67,6 +67,49 @@ export function isFtsCorruptionError(err) {
|
|
|
67
67
|
return /SQLITE_CORRUPT_VTAB/i.test(`${err?.code || ''}`);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* What to do about a damaged FTS5 INDEX — the query-time half of isFtsCorruptionError.
|
|
72
|
+
*
|
|
73
|
+
* R10 P3-9 wired the classifier into `ensureDbWithWalRecovery`, which rebuilds and retries.
|
|
74
|
+
* That covers OPEN time. A structure record damaged inside the index opens fine — nothing
|
|
75
|
+
* reads it until the first MATCH — so the fault surfaces at QUERY time, and there the two
|
|
76
|
+
* faces that carry it to a reader (the CLI catch-all, the MCP safeHandler) passed SQLite's
|
|
77
|
+
* own sentence through with no next step. `recent` / `recall` / `browse` / `context` never
|
|
78
|
+
* touch FTS and keep answering, which makes that dead end easy to misread as "search found
|
|
79
|
+
* nothing" rather than "search is broken".
|
|
80
|
+
*
|
|
81
|
+
* ONE STRING FOR BOTH CHANNELS, unlike the file-level family below. The split there exists
|
|
82
|
+
* because that remedy OVERWRITES the database and must not be handed ready-to-run to an
|
|
83
|
+
* agent holding Bash. This one re-derives every index from its own content table: the rows
|
|
84
|
+
* are never read from the index, so a rebuild is lossless and idempotent, and `doctor`
|
|
85
|
+
* already runs it unprompted. Naming "intact" is load-bearing — the reading this line
|
|
86
|
+
* exists to prevent is "my memories are corrupt".
|
|
87
|
+
*
|
|
88
|
+
* SCOPE, measured rather than assumed: a damaged index does not always reach the caller as
|
|
89
|
+
* SQLITE_CORRUPT_VTAB. `observations_fts_data` holds three rows on a one-observation store —
|
|
90
|
+
* id=1 (averages), id=10 (the STRUCTURE record) and one leaf page — and which row the damage
|
|
91
|
+
* lands on decides the code. Over 100 trials each:
|
|
92
|
+
*
|
|
93
|
+
* UPDATE … SET block = randomblob(32) WHERE id > 1 (structure + leaf) 98 VTAB, 2 NOMEM
|
|
94
|
+
* UPDATE … SET block = randomblob(32) WHERE id > 10 (leaf only) 100 VTAB, 0 NOMEM
|
|
95
|
+
* DELETE … WHERE id > 10 (leaf only) 100 VTAB, 0 NOMEM
|
|
96
|
+
*
|
|
97
|
+
* So NOMEM comes from a mangled STRUCTURE record, where SQLite reads a corrupt varint and asks
|
|
98
|
+
* for an absurd allocation — not from leaf damage. (A first draft of this paragraph said
|
|
99
|
+
* "leaf pages", which would send anyone re-measuring `WHERE id > 10` to 0/N and make the
|
|
100
|
+
* boundary below look vacuous. Caught by the pre-ship claims audit.) That case gets no
|
|
101
|
+
* remedy, on purpose:
|
|
102
|
+
* isFtsCorruptionError is what isDbCorruptionError and isDbUnusableError consult, so
|
|
103
|
+
* admitting SQLITE_NOMEM would answer a real out-of-memory with a full FTS rebuild, and
|
|
104
|
+
* would tell a user their index is damaged when it may be their RAM. The code cannot
|
|
105
|
+
* discriminate the two, so this covers the fault it can name.
|
|
106
|
+
* `tests/fts-corruption-query-time-remedy.test.mjs` pins that boundary.
|
|
107
|
+
*/
|
|
108
|
+
export const FTS_CORRUPTION_REMEDY =
|
|
109
|
+
'The FTS5 search index is damaged; the stored observations are intact. ' +
|
|
110
|
+
'Rebuild it losslessly with `claude-mem-lite fts-check rebuild` ' +
|
|
111
|
+
'(`claude-mem-lite doctor` rebuilds it too, and re-checks everything else).';
|
|
112
|
+
|
|
70
113
|
/**
|
|
71
114
|
* True when `err` means "this file exists and SQLite cannot use it as a database".
|
|
72
115
|
*
|
package/mem-cli.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { ensureDbWithWalRecovery, DB_PATH, DB_DIR, CODE_DIR } from './schema.mjs';
|
|
7
7
|
import { resolveRuntimeDir } from './lib/resolve-data-dir.mjs';
|
|
8
|
+
import { isFtsCorruptionError, FTS_CORRUPTION_REMEDY } from './lib/db-unusable.mjs';
|
|
8
9
|
import { truncate, typeIcon, inferProject, scrubSecrets, COMPRESSED_PENDING_PURGE } from './utils.mjs';
|
|
9
10
|
import { resolveProject } from './project-utils.mjs';
|
|
10
11
|
// READ commands resolve the project DB-aware: a subdirectory whose own name holds no rows
|
|
@@ -3847,6 +3848,12 @@ export async function run(argv) {
|
|
|
3847
3848
|
// agent runs the CLI, the model's context — got a raw Node stack trace. Print the
|
|
3848
3849
|
// message, keep the stack behind CLAUDE_MEM_DEBUG for whoever is actually debugging.
|
|
3849
3850
|
process.stderr.write(`[mem] ${cmd || 'command'} failed: ${(e && e.message) || e}\n`);
|
|
3851
|
+
// A damaged FTS5 index reaches here as SQLITE_CORRUPT_VTAB from the first MATCH.
|
|
3852
|
+
// `fts-check` and `doctor` touch the index too — and both already explain themselves —
|
|
3853
|
+
// so `search` is the one command that DEAD-ENDS on SQLite's sentence, while `recent` /
|
|
3854
|
+
// `recall` / `browse` / `context` / `stats` never read the index and keep working.
|
|
3855
|
+
// The remedy is lossless (see FTS_CORRUPTION_REMEDY); the exit code stays 1.
|
|
3856
|
+
if (isFtsCorruptionError(e)) process.stderr.write(`[mem] ${FTS_CORRUPTION_REMEDY}\n`);
|
|
3850
3857
|
if (process.env.CLAUDE_MEM_DEBUG) process.stderr.write(`${(e && e.stack) || ''}\n`);
|
|
3851
3858
|
process.exitCode = 1;
|
|
3852
3859
|
} finally {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.8.
|
|
3
|
+
"version": "6.8.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "6.8.
|
|
9
|
+
"version": "6.8.3",
|
|
10
10
|
"os": [
|
|
11
11
|
"darwin",
|
|
12
12
|
"linux",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.8.
|
|
3
|
+
"version": "6.8.3",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
package/server.mjs
CHANGED
|
@@ -118,6 +118,7 @@ import { saveWithClosures, formatSupersedeSkipped, formatSupersededNote } from '
|
|
|
118
118
|
import { applyObsUpdate } from './lib/observation-write.mjs';
|
|
119
119
|
import { EXPORT_COLUMNS_SQL, buildExportWhere } from './lib/export-columns.mjs';
|
|
120
120
|
import { recallByFile } from './lib/recall-core.mjs';
|
|
121
|
+
import { isFtsCorruptionError, FTS_CORRUPTION_REMEDY } from './lib/db-unusable.mjs';
|
|
121
122
|
import { fetchRecent } from './lib/recent-core.mjs';
|
|
122
123
|
import { AUTO_MERGE_THRESHOLD } from './lib/dedup-constants.mjs';
|
|
123
124
|
import {
|
|
@@ -374,7 +375,17 @@ function safeHandler(fn, { verbatim = false } = {}) {
|
|
|
374
375
|
const result = await fn(args, extra);
|
|
375
376
|
return verbatim ? result : defangResult(result, { skillBlocks: true });
|
|
376
377
|
} catch (err) {
|
|
377
|
-
|
|
378
|
+
// A damaged FTS5 index arrives here as SQLITE_CORRUPT_VTAB from the first MATCH.
|
|
379
|
+
// Without this the model got SQLite's own sentence and nothing else, on a fault it
|
|
380
|
+
// could have had fixed in one command — and mem_recent / mem_recall / mem_browse
|
|
381
|
+
// keep answering, so the dead end reads as "nothing matched". Both channels carry the
|
|
382
|
+
// same string here, deliberately: unlike the file-level remedy this one is lossless
|
|
383
|
+
// (see FTS_CORRUPTION_REMEDY in lib/db-unusable.mjs).
|
|
384
|
+
const hint = isFtsCorruptionError(err) ? `\n${FTS_CORRUPTION_REMEDY}` : '';
|
|
385
|
+
return defangResult({
|
|
386
|
+
content: [{ type: 'text', text: `Error: ${err.message}${hint}` }],
|
|
387
|
+
isError: true,
|
|
388
|
+
});
|
|
378
389
|
}
|
|
379
390
|
};
|
|
380
391
|
}
|