sage-mem 0.7.1 → 0.7.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/README.md +3 -1
- package/lib/index.js +127 -7
- package/package.json +89 -89
package/README.md
CHANGED
|
@@ -123,6 +123,8 @@ SAGE_MEM_DIR=/path/to/your/memory
|
|
|
123
123
|
- **换个说法也能命中**:打分是**词面重合**(双字组覆盖率),不是语义的——问「我有什么待办」而文件里写的是「代办清单」,差一个字就完全不命中。给这类记忆加一行 `aliases: [待办, todo]` 就能补上
|
|
124
124
|
- **正文超 1500 字符就截断**并标 `…(截断)`,单条记忆不会吃掉整个上下文
|
|
125
125
|
- **同一个问题不会重复注入**(按会话记签名),问第二遍不会又追加一遍
|
|
126
|
+
- **单场会话累计注入到 60 KB 就停**(`MAX_SESSION_BYTES`)。条数上限管的是「一次注多少」,这条管「一场会话总共注多少」—— 长会话里问题一直在换、签名一直在变,累积本来没有上限。到顶之后新的记忆不再注入(这时该让 agent 直接读文件),**压缩或清空之后账面自动归零**,又能注进来
|
|
127
|
+
- **每条记忆的标题带新鲜度**:`(保存于 N 天前)`,**优先读 frontmatter 的 `updated:`,缺失才回落文件 mtime** —— 手写的时间戳是权威,文件系统时间会被复制 / 备份还原 / checkout 整批改掉。理由是陈旧记忆被当事实引用时,「引用」这个动作本身会让它显得更权威而不是更可疑,所以注入文本开头还带一节**引用纪律**(提到路径先确认存在、提到函数先 grep、要动手先核实)。它**独立成节**而不是一条 bullet:Claude Code 实测同一句话埋成 bullet 命中 0/3、独立成节 3/3
|
|
126
128
|
- **`baseline: true` 的记忆每场会话第一步无条件注入**,最多 8 条——给「你是谁」「待办清单」这类每次开场都该在场的记忆用。多条 baseline 之间的先后由 `baselinePriority` 决定(数字越大越前,缺省 0),**不按文件名排**
|
|
127
129
|
- 记忆目录读不到、权限错、frontmatter 格式坏,这些都**降级为不注入并写日志**,不会静默失效
|
|
128
130
|
|
|
@@ -233,7 +235,7 @@ sage-mem 只做三件事,全都在你本机:
|
|
|
233
235
|
|---|---|
|
|
234
236
|
| **读记忆** | 只读 `SAGE_MEM_DIR`(默认 `~/.sage-mem/memory`)下的顶层 `*.md`,单文件上限 512 KB |
|
|
235
237
|
| **写记忆** | 经 agent 的文件工具或设置页写同一个目录;`MEMORY.md`、`session-log.md` 等保留名受保护,设置页碰不到 |
|
|
236
|
-
| **注入** | 把选中的记忆作为上下文交给模型(最多 10 条相关 + 8 条 `baseline
|
|
238
|
+
| **注入** | 把选中的记忆作为上下文交给模型(最多 10 条相关 + 8 条 `baseline`,单场会话累计上限 60 KB) |
|
|
237
239
|
|
|
238
240
|
**它不做什么**:不联网、不开端口、不起常驻进程、不需要任何 API key;默认只读写记忆目录与它上一层的 `.sage-mem/` 状态目录(配置、回滚点、报告)。**例外**:输入源选「记忆 + 会话记录」时,它会额外**读** `$DSH_HOME/sessions` 下的会话记录做定向搜索——那会解压你的对话原文(含私密内容)并送进模型上下文,所以默认不开,见「自动做梦」一节;
|
|
239
241
|
不做事件全捕获(存不存由 agent 按规则判断,不是"你说了什么都记")。
|
package/lib/index.js
CHANGED
|
@@ -60,6 +60,46 @@ const MAX_CHARS_PER_FILE = envInt('SAGE_MEM_MAX_CHARS', 1500, 200, 20000)
|
|
|
60
60
|
*/
|
|
61
61
|
const MAX_BASELINE = envInt('SAGE_MEM_MAX_BASELINE', 8, 1, 50)
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* 单会话累计注入字节上限 —— 到顶以后这个会话就不再注入新的记忆。
|
|
65
|
+
*
|
|
66
|
+
* 为什么要有:条数上限(MAX_RESULTS)管的是「一次」注入多少,管不了「一场会话注
|
|
67
|
+
* 多少次」。长会话里问题一直在换、检索签名一直在变,于是走一步注一批,累积没有
|
|
68
|
+
* 上限 —— 实测「服务器环境」那个问题一次就注 7,700 字符,八次就到 60 KB。
|
|
69
|
+
* Claude Code 有同一层闸门(`MAX_SESSION_BYTES = 60 * 1024`,到顶**彻底停止**预取),
|
|
70
|
+
* 我们此前只有单条上限、没有会话上限。
|
|
71
|
+
*
|
|
72
|
+
* 60 KB 是与 CC 对齐的起点、不是实测出来的数字 —— 留了 SAGE_MEM_MAX_SESSION_BYTES
|
|
73
|
+
* 覆写口,量过再定。
|
|
74
|
+
*/
|
|
75
|
+
const MAX_SESSION_BYTES = envInt('SAGE_MEM_MAX_SESSION_BYTES', 60 * 1024, 4096, 512 * 1024)
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 注入文本里标「保存于 N 天前」的门槛(天)。超了才标;0 = 每条都标。
|
|
79
|
+
*
|
|
80
|
+
* 为什么用「N 天前」而不是日期:模型不擅长日期算术,绝对时间戳几乎不触发陈旧
|
|
81
|
+
* 推理,相对天数才会。为什么需要它:陈旧记忆被当事实引用时,「引用」这个动作
|
|
82
|
+
* 本身会让它显得更权威、而不是更可疑。
|
|
83
|
+
*/
|
|
84
|
+
const STALE_DAYS = envInt('SAGE_MEM_STALE_DAYS', 1, 0, 365)
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 注入文本开头的引用纪律 —— **必须独立成节,不能压成一条 bullet**。
|
|
88
|
+
*
|
|
89
|
+
* Claude Code 对这条做过 A/B:同一句内容埋成 bullet 命中 0/3,独立成节 3/3,
|
|
90
|
+
* 连标题措辞都验过。原因很实在:陈旧记忆被当事实引用时,「引用」这个动作本身
|
|
91
|
+
* 会让它显得更权威而不是更可疑 —— 规矩得在**读这批内容之前**先立。
|
|
92
|
+
*
|
|
93
|
+
* 每次注入都带(约 150 字符),不按会话只带一次:它约束的是「这一批」怎么用,
|
|
94
|
+
* 与批次绑定比与会话绑定更不容易失效。
|
|
95
|
+
*/
|
|
96
|
+
const RECALL_DISCIPLINE = [
|
|
97
|
+
'> **引用纪律**:以下是过去某一刻写下的记录,不是当前事实。',
|
|
98
|
+
'> 提到文件路径先确认存在,提到函数 / 配置项先 grep,要据此动手前先核实当前代码。',
|
|
99
|
+
'> 标了「保存于 N 天前」的尤其注意:其中的 `文件:行号` 很可能已经过期。',
|
|
100
|
+
'> 「记忆里写着 X 存在」不等于「X 现在存在」。',
|
|
101
|
+
].join('\n')
|
|
102
|
+
|
|
63
103
|
/**
|
|
64
104
|
* 结构性保留名:设置页的写/删方法一律不得触碰。
|
|
65
105
|
*
|
|
@@ -173,7 +213,9 @@ async function scanMemoryFiles(ctx, sessionId) {
|
|
|
173
213
|
.map(e => e.name)
|
|
174
214
|
const loaded = await Promise.all(names.map(async (name) => {
|
|
175
215
|
try {
|
|
176
|
-
const
|
|
216
|
+
const full = join(MEMORY_DIR, name)
|
|
217
|
+
// stat 与 readFile 并行 —— 多要一个 mtime 不该变成串行两跳。
|
|
218
|
+
const [content, info] = await Promise.all([readFile(full, 'utf8'), stat(full)])
|
|
177
219
|
const meta = parseFrontmatter(content)
|
|
178
220
|
return {
|
|
179
221
|
file: name,
|
|
@@ -182,6 +224,9 @@ async function scanMemoryFiles(ctx, sessionId) {
|
|
|
182
224
|
aliases: meta.aliases || [],
|
|
183
225
|
baseline: parseBaseline(content),
|
|
184
226
|
baselinePriority: meta.baselinePriority || 0,
|
|
227
|
+
// 新鲜度的两个来源:frontmatter 的 updated(权威)与文件 mtime(退路)。
|
|
228
|
+
updated: meta.updated || '',
|
|
229
|
+
mtimeMs: info.mtimeMs,
|
|
185
230
|
}
|
|
186
231
|
} catch (err) {
|
|
187
232
|
// 只有 ENOENT 是正常状态(扫到读之间被删)。其余——权限错、编码坏、盘掉线——
|
|
@@ -266,7 +311,7 @@ function parseFrontmatter(content) {
|
|
|
266
311
|
// 「baseline 走 parseBaseline 那条带 \r?\n 的正则照旧命中,但这条记忆永远检索不到」,
|
|
267
312
|
// 症状极具欺骗性。下文定义的 parseBaseline 与 extractTitle 早已是 \r?\n。
|
|
268
313
|
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
269
|
-
if (!m) return { name: '', description: '', type: '', tags: [], aliases: [], baselinePriority: 0 }
|
|
314
|
+
if (!m) return { name: '', description: '', type: '', tags: [], aliases: [], updated: '', baselinePriority: 0 }
|
|
270
315
|
const body = m[1]
|
|
271
316
|
const grab = (key) => {
|
|
272
317
|
// [ \t] / [ \t\r] 而不是 \s:\s 会连 CRLF 的 \r 一起吃进捕获组,值尾留一个 \r
|
|
@@ -280,12 +325,21 @@ function parseFrontmatter(content) {
|
|
|
280
325
|
const nested = body.match(/^metadata:\s*\r?\n(?:\s+[^\r\n]+\r?\n)*?\s+type:\s*(\S+)/m)
|
|
281
326
|
if (nested) type = nested[1]
|
|
282
327
|
}
|
|
328
|
+
// `updated` 踩的是与 `type` 同一个坑:它通常写在 `metadata:` 嵌套块里(缩进两格),
|
|
329
|
+
// 顶层 `^updated:` 抓不到 —— 于是「明明写了日期却永远显示不出来」。
|
|
330
|
+
// 抓不到就回落文件 mtime(见 ageDays):手写时间戳是权威的,文件系统时间只是退路。
|
|
331
|
+
let updated = grab('updated')
|
|
332
|
+
if (!updated) {
|
|
333
|
+
const nestedUpdated = body.match(/^metadata:\s*\r?\n(?:\s+[^\r\n]+\r?\n)*?\s+updated:\s*(\S+)/m)
|
|
334
|
+
if (nestedUpdated) updated = nestedUpdated[1]
|
|
335
|
+
}
|
|
283
336
|
return {
|
|
284
337
|
name: grab('name'),
|
|
285
338
|
description: grab('description'),
|
|
286
339
|
type,
|
|
287
340
|
tags: parseTags(body),
|
|
288
341
|
aliases: parseAliases(body),
|
|
342
|
+
updated,
|
|
289
343
|
baselinePriority: parseBaselinePriority(body),
|
|
290
344
|
}
|
|
291
345
|
}
|
|
@@ -337,6 +391,24 @@ function parseBaseline(content) {
|
|
|
337
391
|
return /^(true|yes|1)$/i.test(hit[1].replace(/^["']|["']$/g, '').trim())
|
|
338
392
|
}
|
|
339
393
|
|
|
394
|
+
/**
|
|
395
|
+
* 一条记忆「多久以前写下的」(整数天;null = 无从判断)。
|
|
396
|
+
*
|
|
397
|
+
* **优先 frontmatter 的 `updated`,缺失才回落文件 mtime —— 顺序不能反。**
|
|
398
|
+
* 文件系统时间会被复制、checkout、备份还原整批改掉(我们 memory 目录真被整目录
|
|
399
|
+
* 备份/还原过),它表示的是「这个文件什么时候被动过」,不是「这条记忆什么时候
|
|
400
|
+
* 写的」。手写的时间戳才是权威。
|
|
401
|
+
*
|
|
402
|
+
* 未来日期(时钟偏差 / 手写错)一律当 0:宁可标「今天」也不要标负数。
|
|
403
|
+
*/
|
|
404
|
+
function ageDays(f, nowMs) {
|
|
405
|
+
const fromMeta = Date.parse(f?.updated || '')
|
|
406
|
+
const base = Number.isFinite(fromMeta) ? fromMeta : (Number.isFinite(f?.mtimeMs) ? f.mtimeMs : NaN)
|
|
407
|
+
if (!Number.isFinite(base)) return null
|
|
408
|
+
const days = Math.floor((nowMs - base) / 86400000)
|
|
409
|
+
return days > 0 ? days : 0
|
|
410
|
+
}
|
|
411
|
+
|
|
340
412
|
/**
|
|
341
413
|
* 原子写:先写同目录临时文件,再 rename 覆盖目标。
|
|
342
414
|
*
|
|
@@ -462,6 +534,18 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
462
534
|
const BASELINE_CACHE_MAX = 64
|
|
463
535
|
const baselineInjected = new Set()
|
|
464
536
|
|
|
537
|
+
// 「本会话已经注入掉多少字节」—— 会话级账本,按 session id 记。
|
|
538
|
+
//
|
|
539
|
+
// 为什么不扫会话记录去算:同一轮 assemble 出来的内容会被宿主合进**同一条**
|
|
540
|
+
// user/message(运行时上下文、工作区指令、插件注入共用一条),按消息字节统计
|
|
541
|
+
// 会把别人的内容算进我们的账;按文本找标记又太脆。自己记账是确定性最高的。
|
|
542
|
+
//
|
|
543
|
+
// 为什么要连 messageCount 一起记:压缩 / 清空之后上下文里的记忆已经没了,而
|
|
544
|
+
// session id 不变 —— 只按 id 记账会让「预算用满」变成永久状态,压缩之后再也
|
|
545
|
+
// 注不进来。消息条数变小是压缩发生过的最直接信号,用它把账本归零。
|
|
546
|
+
const INJECT_LEDGER_MAX = 64
|
|
547
|
+
const injectLedger = new Map()
|
|
548
|
+
|
|
465
549
|
// 按问题检索注入:system-prompt/assemble 是异步 waterfall,监听器可 await。
|
|
466
550
|
// 从 agent.session 拿当前 user message,扫 memory 目录按摘要匹配选相关
|
|
467
551
|
// 文件,读全文作为动态 context 段注入——让 agent 第一轮就「想起」。
|
|
@@ -546,19 +630,53 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
546
630
|
|
|
547
631
|
if (picked.length === 0) return assembled
|
|
548
632
|
|
|
549
|
-
|
|
633
|
+
// ── 单会话累计预算 ──
|
|
634
|
+
// 账记在 session id 上;消息条数变小 = 压缩/清空发生过 → 账本归零(见上面
|
|
635
|
+
// injectLedger 的注释)。picked 里 baseline 永远排在检索结果之前,所以预算
|
|
636
|
+
// 不够时先被挤掉的是「更晚才想起来的」那些 —— baseline 有优先权。
|
|
637
|
+
const ledgerKey = session.id || 'no-session-id'
|
|
638
|
+
const prevLedger = injectLedger.get(ledgerKey)
|
|
639
|
+
let usedBytes = prevLedger && messages.length >= prevLedger.messageCount ? prevLedger.bytes : 0
|
|
640
|
+
const nowMs = Date.now()
|
|
641
|
+
|
|
642
|
+
const parts = []
|
|
643
|
+
let skippedForBudget = 0
|
|
644
|
+
for (const f of picked) {
|
|
550
645
|
const body = f.content.length > MAX_CHARS_PER_FILE
|
|
551
646
|
? f.content.slice(0, MAX_CHARS_PER_FILE) + '\n…(截断)'
|
|
552
647
|
: f.content
|
|
553
|
-
|
|
554
|
-
|
|
648
|
+
const days = ageDays(f, nowMs)
|
|
649
|
+
const stamp = days !== null && days >= STALE_DAYS ? `(保存于 ${days} 天前)` : ''
|
|
650
|
+
const part = `### ${f.file}${stamp}\n${body}`
|
|
651
|
+
const size = Buffer.byteLength(part, 'utf8')
|
|
652
|
+
if (usedBytes + size > MAX_SESSION_BYTES) { skippedForBudget++; continue }
|
|
653
|
+
usedBytes += size
|
|
654
|
+
parts.push(part)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// 被预算挡掉了全部条目 —— 一个字都不注。本会话已经注进去的那批还在上下文里,
|
|
658
|
+
// 每步再塞一条「预算已满」只是白花 token;真压缩过之后账本会自己归零。
|
|
659
|
+
if (parts.length === 0) return assembled
|
|
660
|
+
|
|
661
|
+
injectLedger.set(ledgerKey, { bytes: usedBytes, messageCount: messages.length })
|
|
662
|
+
if (injectLedger.size > INJECT_LEDGER_MAX) {
|
|
663
|
+
injectLedger.delete(injectLedger.keys().next().value)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 状态行:让「注了几条、花了多少、被挡掉几条」在上下文里可见 ——
|
|
667
|
+
// 「某条记忆没被注入」是最难查的一类故障,这里留一个可读的出口。
|
|
668
|
+
const status = `(本次注入 ${parts.length} 条;本会话累计 ${Math.round(usedBytes / 1024)} KB` +
|
|
669
|
+
` / 上限 ${Math.round(MAX_SESSION_BYTES / 1024)} KB` +
|
|
670
|
+
(skippedForBudget > 0 ? `;因预算省略 ${skippedForBudget} 条,需要时直接用文件工具读 ${MEMORY_DIR}` : '') +
|
|
671
|
+
')'
|
|
555
672
|
|
|
556
673
|
return {
|
|
557
674
|
...assembled,
|
|
558
675
|
contexts: [...(assembled.contexts ?? []), {
|
|
559
676
|
name: 'sage-mem:recall',
|
|
560
677
|
order: 1000,
|
|
561
|
-
text: `以下是与你当前问题相关的历史记忆(文件式,来自 ${MEMORY_DIR}):\n\n
|
|
678
|
+
text: `以下是与你当前问题相关的历史记忆(文件式,来自 ${MEMORY_DIR}):\n\n` +
|
|
679
|
+
`${RECALL_DISCIPLINE}\n\n${parts.join('\n\n')}\n\n${status}`,
|
|
562
680
|
}],
|
|
563
681
|
}
|
|
564
682
|
} catch (err) {
|
|
@@ -825,7 +943,9 @@ export const __testables = {
|
|
|
825
943
|
parseBaselinePriority,
|
|
826
944
|
parseBaseline,
|
|
827
945
|
parseTags,
|
|
946
|
+
ageDays,
|
|
828
947
|
selectRelevant,
|
|
829
948
|
selectBaseline,
|
|
830
|
-
limits: { MAX_RESULTS, MAX_CHARS_PER_FILE, MAX_BASELINE },
|
|
949
|
+
limits: { MAX_RESULTS, MAX_CHARS_PER_FILE, MAX_BASELINE, MAX_SESSION_BYTES, STALE_DAYS },
|
|
950
|
+
recallDiscipline: RECALL_DISCIPLINE,
|
|
831
951
|
}
|
package/package.json
CHANGED
|
@@ -1,89 +1,89 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "sage-mem",
|
|
3
|
-
"description": "File-based memory plugin for DeepSeek Harness (DSH) — cross-session memory in plain markdown files, with a memory star-map view, type/tag filtering, retrieval aliases, and an optional autodream pass that consolidates memories on demand or on a schedule — with per-run rollback points, a per-file change declaration (what changed and why), and a separately selectable model for the consolidation pass",
|
|
4
|
-
"version": "0.7.
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./lib/index.js",
|
|
9
|
-
"./client": "./lib/client.js",
|
|
10
|
-
"./typert": "./lib/typert.host.js",
|
|
11
|
-
"./package.json": "./package.json"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"lib/",
|
|
15
|
-
"cordis.patch.yml",
|
|
16
|
-
"NOTICE"
|
|
17
|
-
],
|
|
18
|
-
"license": "Apache-2.0",
|
|
19
|
-
"engines": {
|
|
20
|
-
"node": ">=18",
|
|
21
|
-
"dsh": ">=0.1.2-rc.1"
|
|
22
|
-
},
|
|
23
|
-
"dsh": {
|
|
24
|
-
"bundle": {
|
|
25
|
-
"patch": "./cordis.patch.yml"
|
|
26
|
-
},
|
|
27
|
-
"client": {
|
|
28
|
-
"platform": "web",
|
|
29
|
-
"inject": [
|
|
30
|
-
"@deepseek-ai/dsh-client-locale"
|
|
31
|
-
]
|
|
32
|
-
},
|
|
33
|
-
"compatibility": {
|
|
34
|
-
"dsh": ">=0.1.2-rc.1",
|
|
35
|
-
"dshReleases": {
|
|
36
|
-
"0.1.2-rc.1": "compatible",
|
|
37
|
-
"0.1.3-alpha.1": "compatible",
|
|
38
|
-
"0.1.3-alpha.2": "compatible",
|
|
39
|
-
"0.1.5-rc.1": "compatible",
|
|
40
|
-
"0.1.6-alpha.1": "compatible",
|
|
41
|
-
"0.1.6-alpha.2": "compatible",
|
|
42
|
-
"0.1.7-rc.1": "compatible",
|
|
43
|
-
"0.1.7-rc.2": "compatible",
|
|
44
|
-
"0.2.0-rc.1": "compatible",
|
|
45
|
-
"0.2.0-rc.2": "compatible"
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
"dependencies": {
|
|
50
|
-
"zod": "^4.4.3"
|
|
51
|
-
},
|
|
52
|
-
"devDependencies": {
|
|
53
|
-
"react": "^18.0.0"
|
|
54
|
-
},
|
|
55
|
-
"peerDependencies": {
|
|
56
|
-
"@deepseek-ai/cordis": "^4.0.2",
|
|
57
|
-
"@deepseek-ai/dsh-typert-protocol": ">=0.1.2-rc.1",
|
|
58
|
-
"@deepseek-ai/dsh-client-locale": ">=0.1.2-rc.1",
|
|
59
|
-
"react": "^18.0.0"
|
|
60
|
-
},
|
|
61
|
-
"repository": {
|
|
62
|
-
"type": "git",
|
|
63
|
-
"url": "git+https://github.com/gezi-wen/sage-mem.git"
|
|
64
|
-
},
|
|
65
|
-
"homepage": "https://github.com/gezi-wen/sage-mem#readme",
|
|
66
|
-
"bugs": {
|
|
67
|
-
"url": "https://github.com/gezi-wen/sage-mem/issues"
|
|
68
|
-
},
|
|
69
|
-
"keywords": [
|
|
70
|
-
"memory",
|
|
71
|
-
"ai-memory",
|
|
72
|
-
"agent-memory",
|
|
73
|
-
"long-term-memory",
|
|
74
|
-
"cross-session-memory",
|
|
75
|
-
"context-management",
|
|
76
|
-
"knowledge-management",
|
|
77
|
-
"markdown",
|
|
78
|
-
"dsh",
|
|
79
|
-
"dsh-plugin",
|
|
80
|
-
"deepseek",
|
|
81
|
-
"deepseek-harness",
|
|
82
|
-
"cordis",
|
|
83
|
-
"typert",
|
|
84
|
-
"plugin",
|
|
85
|
-
"ai-agent",
|
|
86
|
-
"agent",
|
|
87
|
-
"claude-code"
|
|
88
|
-
]
|
|
89
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "sage-mem",
|
|
3
|
+
"description": "File-based memory plugin for DeepSeek Harness (DSH) — cross-session memory in plain markdown files, with a memory star-map view, type/tag filtering, retrieval aliases, and an optional autodream pass that consolidates memories on demand or on a schedule — with per-run rollback points, a per-file change declaration (what changed and why), and a separately selectable model for the consolidation pass",
|
|
4
|
+
"version": "0.7.2",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./typert": "./lib/typert.host.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"lib/",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"NOTICE"
|
|
17
|
+
],
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18",
|
|
21
|
+
"dsh": ">=0.1.2-rc.1"
|
|
22
|
+
},
|
|
23
|
+
"dsh": {
|
|
24
|
+
"bundle": {
|
|
25
|
+
"patch": "./cordis.patch.yml"
|
|
26
|
+
},
|
|
27
|
+
"client": {
|
|
28
|
+
"platform": "web",
|
|
29
|
+
"inject": [
|
|
30
|
+
"@deepseek-ai/dsh-client-locale"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"compatibility": {
|
|
34
|
+
"dsh": ">=0.1.2-rc.1",
|
|
35
|
+
"dshReleases": {
|
|
36
|
+
"0.1.2-rc.1": "compatible",
|
|
37
|
+
"0.1.3-alpha.1": "compatible",
|
|
38
|
+
"0.1.3-alpha.2": "compatible",
|
|
39
|
+
"0.1.5-rc.1": "compatible",
|
|
40
|
+
"0.1.6-alpha.1": "compatible",
|
|
41
|
+
"0.1.6-alpha.2": "compatible",
|
|
42
|
+
"0.1.7-rc.1": "compatible",
|
|
43
|
+
"0.1.7-rc.2": "compatible",
|
|
44
|
+
"0.2.0-rc.1": "compatible",
|
|
45
|
+
"0.2.0-rc.2": "compatible"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"zod": "^4.4.3"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"react": "^18.0.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
57
|
+
"@deepseek-ai/dsh-typert-protocol": ">=0.1.2-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.2-rc.1",
|
|
59
|
+
"react": "^18.0.0"
|
|
60
|
+
},
|
|
61
|
+
"repository": {
|
|
62
|
+
"type": "git",
|
|
63
|
+
"url": "git+https://github.com/gezi-wen/sage-mem.git"
|
|
64
|
+
},
|
|
65
|
+
"homepage": "https://github.com/gezi-wen/sage-mem#readme",
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/gezi-wen/sage-mem/issues"
|
|
68
|
+
},
|
|
69
|
+
"keywords": [
|
|
70
|
+
"memory",
|
|
71
|
+
"ai-memory",
|
|
72
|
+
"agent-memory",
|
|
73
|
+
"long-term-memory",
|
|
74
|
+
"cross-session-memory",
|
|
75
|
+
"context-management",
|
|
76
|
+
"knowledge-management",
|
|
77
|
+
"markdown",
|
|
78
|
+
"dsh",
|
|
79
|
+
"dsh-plugin",
|
|
80
|
+
"deepseek",
|
|
81
|
+
"deepseek-harness",
|
|
82
|
+
"cordis",
|
|
83
|
+
"typert",
|
|
84
|
+
"plugin",
|
|
85
|
+
"ai-agent",
|
|
86
|
+
"agent",
|
|
87
|
+
"claude-code"
|
|
88
|
+
]
|
|
89
|
+
}
|