sage-mem 0.4.0 → 0.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/LICENSE +202 -202
- package/NOTICE +5 -5
- package/README.md +138 -115
- package/cordis.patch.yml +4 -4
- package/lib/client.js +432 -432
- package/lib/index.js +212 -212
- package/lib/typert.host.js +90 -90
- package/package.json +68 -52
package/lib/index.js
CHANGED
|
@@ -1,212 +1,212 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* sage-mem — 文件式记忆插件(CC 原生方式)。
|
|
3
|
-
*
|
|
4
|
-
* 记忆存在本地 markdown 文件目录(frontmatter + 正文,4 类),不是数据库。
|
|
5
|
-
* host 侧两件事:
|
|
6
|
-
* 1. 按问题检索注入:system-prompt/assemble 时扫 memory 目录,按双字匹配
|
|
7
|
-
* 选相关文件,读全文注入 system prompt,让 agent 第一轮就「想起」
|
|
8
|
-
* 2. TypertRemoteService:给设置页「记忆管理」提供文件列表/读/写/删
|
|
9
|
-
*
|
|
10
|
-
* 写入靠 AGENTS.md 的「记忆使用」规则引导 agent 自己判断 + 用文件工具写
|
|
11
|
-
* memory 目录——透明、可检查、防膨胀。无 worker、无 SQLite、无端口。
|
|
12
|
-
*
|
|
13
|
-
* 旧 worker 版见 git 分支 `sqlite-worker`。
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
|
17
|
-
import { homedir } from 'node:os'
|
|
18
|
-
import { readFile, readdir, writeFile, unlink } from 'node:fs/promises'
|
|
19
|
-
import { join, basename } from 'node:path'
|
|
20
|
-
|
|
21
|
-
const MEMORY_DIR = process.env.SAGE_MEM_DIR || join(homedir(), '.sage-mem', 'memory')
|
|
22
|
-
const MAX_RESULTS = 5
|
|
23
|
-
const MAX_CHARS_PER_FILE = 1500
|
|
24
|
-
|
|
25
|
-
/** 提取消息文本,兼容 content 为字符串或 text-block 数组两种形态。 */
|
|
26
|
-
function extractText(content) {
|
|
27
|
-
if (typeof content === 'string') return content.trim()
|
|
28
|
-
if (!Array.isArray(content)) return ''
|
|
29
|
-
return content
|
|
30
|
-
.filter(b => b && b.type === 'text' && typeof b.text === 'string')
|
|
31
|
-
.map(b => b.text)
|
|
32
|
-
.join('\n')
|
|
33
|
-
.trim()
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** 提取双字(2-gram)集合,中英文数字通用。 */
|
|
37
|
-
function bigrams(s) {
|
|
38
|
-
const clean = String(s).toLowerCase().replace(/[^\p{L}\p{N}]+/gu, '')
|
|
39
|
-
const set = new Set()
|
|
40
|
-
for (let i = 0; i < clean.length - 1; i++) set.add(clean.slice(i, i + 2))
|
|
41
|
-
return set
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** query 的双字在 text 中的覆盖率(0~1),作为相关性得分。 */
|
|
45
|
-
function score(query, text) {
|
|
46
|
-
const q = bigrams(query)
|
|
47
|
-
if (q.size === 0) return 0
|
|
48
|
-
const t = bigrams(text)
|
|
49
|
-
let hit = 0
|
|
50
|
-
for (const g of q) if (t.has(g)) hit++
|
|
51
|
-
return hit / q.size
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** 扫 memory 目录,读回全部记忆文件的正文(跳过索引与子目录)。 */
|
|
55
|
-
async function scanMemoryFiles() {
|
|
56
|
-
try {
|
|
57
|
-
const entries = await readdir(MEMORY_DIR, { withFileTypes: true })
|
|
58
|
-
const names = entries
|
|
59
|
-
.filter(e => e.isFile() && e.name.endsWith('.md') && e.name !== 'MEMORY.md')
|
|
60
|
-
.map(e => e.name)
|
|
61
|
-
const loaded = await Promise.all(names.map(async (name) => {
|
|
62
|
-
try {
|
|
63
|
-
const content = await readFile(join(MEMORY_DIR, name), 'utf8')
|
|
64
|
-
return { file: name, content }
|
|
65
|
-
} catch {
|
|
66
|
-
return null
|
|
67
|
-
}
|
|
68
|
-
}))
|
|
69
|
-
return loaded.filter(Boolean)
|
|
70
|
-
} catch {
|
|
71
|
-
return []
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** 按得分选相关文件,降序取 top N。 */
|
|
76
|
-
function selectRelevant(query, files) {
|
|
77
|
-
return files
|
|
78
|
-
.map(f => ({ ...f, score: score(query, f.content) }))
|
|
79
|
-
.filter(f => f.score > 0)
|
|
80
|
-
.sort((a, b) => b.score - a.score)
|
|
81
|
-
.slice(0, MAX_RESULTS)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** 解析 frontmatter 的 name/description/type(兼容顶层与 metadata 嵌套两种 type)。 */
|
|
85
|
-
function parseFrontmatter(content) {
|
|
86
|
-
const m = content.match(/^---\n([\s\S]*?)\n---/)
|
|
87
|
-
if (!m) return { name: '', description: '', type: '' }
|
|
88
|
-
const body = m[1]
|
|
89
|
-
const grab = (key) => {
|
|
90
|
-
const hit = body.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))
|
|
91
|
-
if (!hit) return ''
|
|
92
|
-
return hit[1].replace(/^["']|["']$/g, '').trim()
|
|
93
|
-
}
|
|
94
|
-
let type = grab('type')
|
|
95
|
-
if (!type) {
|
|
96
|
-
const nested = body.match(/^metadata:\s*\n(?:\s+[^\n]+\n)*?\s+type:\s*(\S+)/m)
|
|
97
|
-
if (nested) type = nested[1]
|
|
98
|
-
}
|
|
99
|
-
return { name: grab('name'), description: grab('description'), type }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** 防路径穿越:只取 basename,且必须在 memory 目录内(.md,排除索引)。 */
|
|
103
|
-
function safeName(raw) {
|
|
104
|
-
const name = basename(String(raw || ''))
|
|
105
|
-
if (!name || !name.endsWith('.md') || name === 'MEMORY.md') return null
|
|
106
|
-
return name
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 手动 @Remote marker 注册(Node 24 不解析装饰器语法,手写 decorator context)。
|
|
111
|
-
*/
|
|
112
|
-
function markRemote(cls, method, exportName) {
|
|
113
|
-
const instance = Object.create(cls.prototype)
|
|
114
|
-
Remote(exportName)(undefined, {
|
|
115
|
-
kind: 'method',
|
|
116
|
-
name: method,
|
|
117
|
-
private: false,
|
|
118
|
-
static: false,
|
|
119
|
-
addInitializer(fn) {
|
|
120
|
-
fn.call(instance)
|
|
121
|
-
},
|
|
122
|
-
})
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export class MemoryGateway extends TypertRemoteService {
|
|
126
|
-
constructor(ctx) {
|
|
127
|
-
super(ctx, 'memory')
|
|
128
|
-
|
|
129
|
-
// 按问题检索注入:system-prompt/assemble 是异步 waterfall,监听器可 await。
|
|
130
|
-
// 从 agent.session 拿当前 user message,扫 memory 目录按双字匹配选相关
|
|
131
|
-
// 文件,读全文作为动态 context 段注入——让 agent 第一轮就「想起」。
|
|
132
|
-
ctx.on('system-prompt/assemble', async (assembly, context, next) => {
|
|
133
|
-
const assembled = await next()
|
|
134
|
-
try {
|
|
135
|
-
const agent = context?.agent
|
|
136
|
-
const session = agent?.session
|
|
137
|
-
if (!session || typeof session.deriveMessages !== 'function') return assembled
|
|
138
|
-
const messages = [...session.deriveMessages()]
|
|
139
|
-
const lastUser = messages.reverse().find(m => m?.role === 'user')
|
|
140
|
-
const text = extractText(lastUser?.content)
|
|
141
|
-
if (!text || text.length < 2) return assembled
|
|
142
|
-
|
|
143
|
-
const files = await scanMemoryFiles()
|
|
144
|
-
const relevant = selectRelevant(text, files)
|
|
145
|
-
if (relevant.length === 0) return assembled
|
|
146
|
-
|
|
147
|
-
const parts = relevant.map(f => {
|
|
148
|
-
const body = f.content.length > MAX_CHARS_PER_FILE
|
|
149
|
-
? f.content.slice(0, MAX_CHARS_PER_FILE) + '\n…(截断)'
|
|
150
|
-
: f.content
|
|
151
|
-
return `### ${f.file}\n${body}`
|
|
152
|
-
})
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
...assembled,
|
|
156
|
-
contexts: [...(assembled.contexts ?? []), {
|
|
157
|
-
name: 'sage-mem:recall',
|
|
158
|
-
order: 1000,
|
|
159
|
-
text: `以下是与你当前问题相关的历史记忆(文件式,来自 ${MEMORY_DIR}):\n\n${parts.join('\n\n')}`,
|
|
160
|
-
}],
|
|
161
|
-
}
|
|
162
|
-
} catch {
|
|
163
|
-
return assembled
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** 列出 memory 目录全部记忆文件(文件名 + 类型 + 描述 + 大小)。 */
|
|
169
|
-
async listFiles() {
|
|
170
|
-
const files = await scanMemoryFiles()
|
|
171
|
-
return files.map(f => {
|
|
172
|
-
const meta = parseFrontmatter(f.content)
|
|
173
|
-
return {
|
|
174
|
-
file: f.file,
|
|
175
|
-
type: meta.type || 'reference',
|
|
176
|
-
description: meta.description || '',
|
|
177
|
-
size: f.content.length,
|
|
178
|
-
}
|
|
179
|
-
})
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** 读单个记忆文件全文。 */
|
|
183
|
-
async readFile(name) {
|
|
184
|
-
const safe = safeName(name)
|
|
185
|
-
if (!safe) throw new Error('sage-mem: invalid file name')
|
|
186
|
-
const content = await readFile(join(MEMORY_DIR, safe), 'utf8')
|
|
187
|
-
return { name: safe, content }
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** 写(新增或覆盖)单个记忆文件。 */
|
|
191
|
-
async writeFile(name, content) {
|
|
192
|
-
const safe = safeName(name)
|
|
193
|
-
if (!safe) throw new Error('sage-mem: invalid file name')
|
|
194
|
-
await writeFile(join(MEMORY_DIR, safe), String(content ?? ''), 'utf8')
|
|
195
|
-
return { ok: true, file: safe }
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** 删除单个记忆文件。 */
|
|
199
|
-
async deleteFile(name) {
|
|
200
|
-
const safe = safeName(name)
|
|
201
|
-
if (!safe) throw new Error('sage-mem: invalid file name')
|
|
202
|
-
await unlink(join(MEMORY_DIR, safe))
|
|
203
|
-
return { ok: true }
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
markRemote(MemoryGateway, 'listFiles', 'listFiles')
|
|
208
|
-
markRemote(MemoryGateway, 'readFile', 'readFile')
|
|
209
|
-
markRemote(MemoryGateway, 'writeFile', 'writeFile')
|
|
210
|
-
markRemote(MemoryGateway, 'deleteFile', 'deleteFile')
|
|
211
|
-
|
|
212
|
-
export default MemoryGateway
|
|
1
|
+
/**
|
|
2
|
+
* sage-mem — 文件式记忆插件(CC 原生方式)。
|
|
3
|
+
*
|
|
4
|
+
* 记忆存在本地 markdown 文件目录(frontmatter + 正文,4 类),不是数据库。
|
|
5
|
+
* host 侧两件事:
|
|
6
|
+
* 1. 按问题检索注入:system-prompt/assemble 时扫 memory 目录,按双字匹配
|
|
7
|
+
* 选相关文件,读全文注入 system prompt,让 agent 第一轮就「想起」
|
|
8
|
+
* 2. TypertRemoteService:给设置页「记忆管理」提供文件列表/读/写/删
|
|
9
|
+
*
|
|
10
|
+
* 写入靠 AGENTS.md 的「记忆使用」规则引导 agent 自己判断 + 用文件工具写
|
|
11
|
+
* memory 目录——透明、可检查、防膨胀。无 worker、无 SQLite、无端口。
|
|
12
|
+
*
|
|
13
|
+
* 旧 worker 版见 git 分支 `sqlite-worker`。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
|
17
|
+
import { homedir } from 'node:os'
|
|
18
|
+
import { readFile, readdir, writeFile, unlink } from 'node:fs/promises'
|
|
19
|
+
import { join, basename } from 'node:path'
|
|
20
|
+
|
|
21
|
+
const MEMORY_DIR = process.env.SAGE_MEM_DIR || join(homedir(), '.sage-mem', 'memory')
|
|
22
|
+
const MAX_RESULTS = 5
|
|
23
|
+
const MAX_CHARS_PER_FILE = 1500
|
|
24
|
+
|
|
25
|
+
/** 提取消息文本,兼容 content 为字符串或 text-block 数组两种形态。 */
|
|
26
|
+
function extractText(content) {
|
|
27
|
+
if (typeof content === 'string') return content.trim()
|
|
28
|
+
if (!Array.isArray(content)) return ''
|
|
29
|
+
return content
|
|
30
|
+
.filter(b => b && b.type === 'text' && typeof b.text === 'string')
|
|
31
|
+
.map(b => b.text)
|
|
32
|
+
.join('\n')
|
|
33
|
+
.trim()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 提取双字(2-gram)集合,中英文数字通用。 */
|
|
37
|
+
function bigrams(s) {
|
|
38
|
+
const clean = String(s).toLowerCase().replace(/[^\p{L}\p{N}]+/gu, '')
|
|
39
|
+
const set = new Set()
|
|
40
|
+
for (let i = 0; i < clean.length - 1; i++) set.add(clean.slice(i, i + 2))
|
|
41
|
+
return set
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** query 的双字在 text 中的覆盖率(0~1),作为相关性得分。 */
|
|
45
|
+
function score(query, text) {
|
|
46
|
+
const q = bigrams(query)
|
|
47
|
+
if (q.size === 0) return 0
|
|
48
|
+
const t = bigrams(text)
|
|
49
|
+
let hit = 0
|
|
50
|
+
for (const g of q) if (t.has(g)) hit++
|
|
51
|
+
return hit / q.size
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 扫 memory 目录,读回全部记忆文件的正文(跳过索引与子目录)。 */
|
|
55
|
+
async function scanMemoryFiles() {
|
|
56
|
+
try {
|
|
57
|
+
const entries = await readdir(MEMORY_DIR, { withFileTypes: true })
|
|
58
|
+
const names = entries
|
|
59
|
+
.filter(e => e.isFile() && e.name.endsWith('.md') && e.name !== 'MEMORY.md' && e.name !== 'session-log.md')
|
|
60
|
+
.map(e => e.name)
|
|
61
|
+
const loaded = await Promise.all(names.map(async (name) => {
|
|
62
|
+
try {
|
|
63
|
+
const content = await readFile(join(MEMORY_DIR, name), 'utf8')
|
|
64
|
+
return { file: name, content }
|
|
65
|
+
} catch {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}))
|
|
69
|
+
return loaded.filter(Boolean)
|
|
70
|
+
} catch {
|
|
71
|
+
return []
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 按得分选相关文件,降序取 top N。 */
|
|
76
|
+
function selectRelevant(query, files) {
|
|
77
|
+
return files
|
|
78
|
+
.map(f => ({ ...f, score: score(query, f.content) }))
|
|
79
|
+
.filter(f => f.score > 0)
|
|
80
|
+
.sort((a, b) => b.score - a.score)
|
|
81
|
+
.slice(0, MAX_RESULTS)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 解析 frontmatter 的 name/description/type(兼容顶层与 metadata 嵌套两种 type)。 */
|
|
85
|
+
function parseFrontmatter(content) {
|
|
86
|
+
const m = content.match(/^---\n([\s\S]*?)\n---/)
|
|
87
|
+
if (!m) return { name: '', description: '', type: '' }
|
|
88
|
+
const body = m[1]
|
|
89
|
+
const grab = (key) => {
|
|
90
|
+
const hit = body.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))
|
|
91
|
+
if (!hit) return ''
|
|
92
|
+
return hit[1].replace(/^["']|["']$/g, '').trim()
|
|
93
|
+
}
|
|
94
|
+
let type = grab('type')
|
|
95
|
+
if (!type) {
|
|
96
|
+
const nested = body.match(/^metadata:\s*\n(?:\s+[^\n]+\n)*?\s+type:\s*(\S+)/m)
|
|
97
|
+
if (nested) type = nested[1]
|
|
98
|
+
}
|
|
99
|
+
return { name: grab('name'), description: grab('description'), type }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 防路径穿越:只取 basename,且必须在 memory 目录内(.md,排除索引)。 */
|
|
103
|
+
function safeName(raw) {
|
|
104
|
+
const name = basename(String(raw || ''))
|
|
105
|
+
if (!name || !name.endsWith('.md') || name === 'MEMORY.md') return null
|
|
106
|
+
return name
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 手动 @Remote marker 注册(Node 24 不解析装饰器语法,手写 decorator context)。
|
|
111
|
+
*/
|
|
112
|
+
function markRemote(cls, method, exportName) {
|
|
113
|
+
const instance = Object.create(cls.prototype)
|
|
114
|
+
Remote(exportName)(undefined, {
|
|
115
|
+
kind: 'method',
|
|
116
|
+
name: method,
|
|
117
|
+
private: false,
|
|
118
|
+
static: false,
|
|
119
|
+
addInitializer(fn) {
|
|
120
|
+
fn.call(instance)
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class MemoryGateway extends TypertRemoteService {
|
|
126
|
+
constructor(ctx) {
|
|
127
|
+
super(ctx, 'memory')
|
|
128
|
+
|
|
129
|
+
// 按问题检索注入:system-prompt/assemble 是异步 waterfall,监听器可 await。
|
|
130
|
+
// 从 agent.session 拿当前 user message,扫 memory 目录按双字匹配选相关
|
|
131
|
+
// 文件,读全文作为动态 context 段注入——让 agent 第一轮就「想起」。
|
|
132
|
+
ctx.on('system-prompt/assemble', async (assembly, context, next) => {
|
|
133
|
+
const assembled = await next()
|
|
134
|
+
try {
|
|
135
|
+
const agent = context?.agent
|
|
136
|
+
const session = agent?.session
|
|
137
|
+
if (!session || typeof session.deriveMessages !== 'function') return assembled
|
|
138
|
+
const messages = [...session.deriveMessages()]
|
|
139
|
+
const lastUser = messages.reverse().find(m => m?.role === 'user')
|
|
140
|
+
const text = extractText(lastUser?.content)
|
|
141
|
+
if (!text || text.length < 2) return assembled
|
|
142
|
+
|
|
143
|
+
const files = await scanMemoryFiles()
|
|
144
|
+
const relevant = selectRelevant(text, files)
|
|
145
|
+
if (relevant.length === 0) return assembled
|
|
146
|
+
|
|
147
|
+
const parts = relevant.map(f => {
|
|
148
|
+
const body = f.content.length > MAX_CHARS_PER_FILE
|
|
149
|
+
? f.content.slice(0, MAX_CHARS_PER_FILE) + '\n…(截断)'
|
|
150
|
+
: f.content
|
|
151
|
+
return `### ${f.file}\n${body}`
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
...assembled,
|
|
156
|
+
contexts: [...(assembled.contexts ?? []), {
|
|
157
|
+
name: 'sage-mem:recall',
|
|
158
|
+
order: 1000,
|
|
159
|
+
text: `以下是与你当前问题相关的历史记忆(文件式,来自 ${MEMORY_DIR}):\n\n${parts.join('\n\n')}`,
|
|
160
|
+
}],
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
return assembled
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 列出 memory 目录全部记忆文件(文件名 + 类型 + 描述 + 大小)。 */
|
|
169
|
+
async listFiles() {
|
|
170
|
+
const files = await scanMemoryFiles()
|
|
171
|
+
return files.map(f => {
|
|
172
|
+
const meta = parseFrontmatter(f.content)
|
|
173
|
+
return {
|
|
174
|
+
file: f.file,
|
|
175
|
+
type: meta.type || 'reference',
|
|
176
|
+
description: meta.description || '',
|
|
177
|
+
size: f.content.length,
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** 读单个记忆文件全文。 */
|
|
183
|
+
async readFile(name) {
|
|
184
|
+
const safe = safeName(name)
|
|
185
|
+
if (!safe) throw new Error('sage-mem: invalid file name')
|
|
186
|
+
const content = await readFile(join(MEMORY_DIR, safe), 'utf8')
|
|
187
|
+
return { name: safe, content }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 写(新增或覆盖)单个记忆文件。 */
|
|
191
|
+
async writeFile(name, content) {
|
|
192
|
+
const safe = safeName(name)
|
|
193
|
+
if (!safe) throw new Error('sage-mem: invalid file name')
|
|
194
|
+
await writeFile(join(MEMORY_DIR, safe), String(content ?? ''), 'utf8')
|
|
195
|
+
return { ok: true, file: safe }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** 删除单个记忆文件。 */
|
|
199
|
+
async deleteFile(name) {
|
|
200
|
+
const safe = safeName(name)
|
|
201
|
+
if (!safe) throw new Error('sage-mem: invalid file name')
|
|
202
|
+
await unlink(join(MEMORY_DIR, safe))
|
|
203
|
+
return { ok: true }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
markRemote(MemoryGateway, 'listFiles', 'listFiles')
|
|
208
|
+
markRemote(MemoryGateway, 'readFile', 'readFile')
|
|
209
|
+
markRemote(MemoryGateway, 'writeFile', 'writeFile')
|
|
210
|
+
markRemote(MemoryGateway, 'deleteFile', 'deleteFile')
|
|
211
|
+
|
|
212
|
+
export default MemoryGateway
|
package/lib/typert.host.js
CHANGED
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* sage-mem — 手写 strict typert host manifest(对齐官方生成文件格式)。
|
|
3
|
-
* typert-loader 在插件挂载时读 package.json 的 exports["./typert"],
|
|
4
|
-
* import 本文件并把 TYPERT 注册进 ctx.typert.local(strict 定义)。
|
|
5
|
-
* schema 必须是 zod v4 实例(typert-loader 校验 "_zod" in schema)。
|
|
6
|
-
*/
|
|
7
|
-
import { z } from 'zod'
|
|
8
|
-
|
|
9
|
-
const fileNameSchema = z.string()
|
|
10
|
-
const fileContentSchema = z.string()
|
|
11
|
-
|
|
12
|
-
const fileSchema = z.object({
|
|
13
|
-
'file': z.string().readonly(),
|
|
14
|
-
'type': z.string().readonly(),
|
|
15
|
-
'description': z.string().readonly(),
|
|
16
|
-
'size': z.number().readonly(),
|
|
17
|
-
}).readonly()
|
|
18
|
-
|
|
19
|
-
const listResultSchema = z.array(fileSchema)
|
|
20
|
-
const readResultSchema = z.object({
|
|
21
|
-
'name': z.string().readonly(),
|
|
22
|
-
'content': z.string().readonly(),
|
|
23
|
-
}).readonly()
|
|
24
|
-
const writeResultSchema = z.object({
|
|
25
|
-
'ok': z.boolean().readonly(),
|
|
26
|
-
'file': z.string().readonly(),
|
|
27
|
-
}).readonly()
|
|
28
|
-
const deleteResultSchema = z.object({
|
|
29
|
-
'ok': z.boolean().readonly(),
|
|
30
|
-
}).readonly()
|
|
31
|
-
|
|
32
|
-
export const TYPERT = {
|
|
33
|
-
package: 'sage-mem',
|
|
34
|
-
face: 'host',
|
|
35
|
-
schemas: [],
|
|
36
|
-
invocations: [
|
|
37
|
-
{
|
|
38
|
-
id: 'sage-mem#memory/listFiles',
|
|
39
|
-
service: 'memory',
|
|
40
|
-
namespace: 'memory',
|
|
41
|
-
method: 'listFiles',
|
|
42
|
-
invocation: { kind: 'direct' },
|
|
43
|
-
parameters: [],
|
|
44
|
-
result: { mode: 'strict', typeSymbol: 'sage-mem/types#FileList', schema: listResultSchema },
|
|
45
|
-
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
id: 'sage-mem#memory/readFile',
|
|
49
|
-
service: 'memory',
|
|
50
|
-
namespace: 'memory',
|
|
51
|
-
method: 'readFile',
|
|
52
|
-
invocation: { kind: 'direct' },
|
|
53
|
-
parameters: [
|
|
54
|
-
{ name: 'name', wire: 'name', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#FileName', schema: fileNameSchema } },
|
|
55
|
-
],
|
|
56
|
-
result: { mode: 'strict', typeSymbol: 'sage-mem/types#FileContent', schema: readResultSchema },
|
|
57
|
-
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
id: 'sage-mem#memory/writeFile',
|
|
61
|
-
service: 'memory',
|
|
62
|
-
namespace: 'memory',
|
|
63
|
-
method: 'writeFile',
|
|
64
|
-
invocation: { kind: 'direct' },
|
|
65
|
-
parameters: [
|
|
66
|
-
{ name: 'name', wire: 'name', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#FileName', schema: fileNameSchema } },
|
|
67
|
-
{ name: 'content', wire: 'content', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#FileContent', schema: fileContentSchema } },
|
|
68
|
-
],
|
|
69
|
-
result: { mode: 'strict', typeSymbol: 'sage-mem/types#WriteResult', schema: writeResultSchema },
|
|
70
|
-
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
id: 'sage-mem#memory/deleteFile',
|
|
74
|
-
service: 'memory',
|
|
75
|
-
namespace: 'memory',
|
|
76
|
-
method: 'deleteFile',
|
|
77
|
-
invocation: { kind: 'direct' },
|
|
78
|
-
parameters: [
|
|
79
|
-
{ name: 'name', wire: 'name', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#FileName', schema: fileNameSchema } },
|
|
80
|
-
],
|
|
81
|
-
result: { mode: 'strict', typeSymbol: 'sage-mem/types#DeleteResult', schema: deleteResultSchema },
|
|
82
|
-
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
model: {
|
|
86
|
-
"services": [],
|
|
87
|
-
"events": [],
|
|
88
|
-
"objects": [],
|
|
89
|
-
},
|
|
90
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* sage-mem — 手写 strict typert host manifest(对齐官方生成文件格式)。
|
|
3
|
+
* typert-loader 在插件挂载时读 package.json 的 exports["./typert"],
|
|
4
|
+
* import 本文件并把 TYPERT 注册进 ctx.typert.local(strict 定义)。
|
|
5
|
+
* schema 必须是 zod v4 实例(typert-loader 校验 "_zod" in schema)。
|
|
6
|
+
*/
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
|
|
9
|
+
const fileNameSchema = z.string()
|
|
10
|
+
const fileContentSchema = z.string()
|
|
11
|
+
|
|
12
|
+
const fileSchema = z.object({
|
|
13
|
+
'file': z.string().readonly(),
|
|
14
|
+
'type': z.string().readonly(),
|
|
15
|
+
'description': z.string().readonly(),
|
|
16
|
+
'size': z.number().readonly(),
|
|
17
|
+
}).readonly()
|
|
18
|
+
|
|
19
|
+
const listResultSchema = z.array(fileSchema)
|
|
20
|
+
const readResultSchema = z.object({
|
|
21
|
+
'name': z.string().readonly(),
|
|
22
|
+
'content': z.string().readonly(),
|
|
23
|
+
}).readonly()
|
|
24
|
+
const writeResultSchema = z.object({
|
|
25
|
+
'ok': z.boolean().readonly(),
|
|
26
|
+
'file': z.string().readonly(),
|
|
27
|
+
}).readonly()
|
|
28
|
+
const deleteResultSchema = z.object({
|
|
29
|
+
'ok': z.boolean().readonly(),
|
|
30
|
+
}).readonly()
|
|
31
|
+
|
|
32
|
+
export const TYPERT = {
|
|
33
|
+
package: 'sage-mem',
|
|
34
|
+
face: 'host',
|
|
35
|
+
schemas: [],
|
|
36
|
+
invocations: [
|
|
37
|
+
{
|
|
38
|
+
id: 'sage-mem#memory/listFiles',
|
|
39
|
+
service: 'memory',
|
|
40
|
+
namespace: 'memory',
|
|
41
|
+
method: 'listFiles',
|
|
42
|
+
invocation: { kind: 'direct' },
|
|
43
|
+
parameters: [],
|
|
44
|
+
result: { mode: 'strict', typeSymbol: 'sage-mem/types#FileList', schema: listResultSchema },
|
|
45
|
+
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'sage-mem#memory/readFile',
|
|
49
|
+
service: 'memory',
|
|
50
|
+
namespace: 'memory',
|
|
51
|
+
method: 'readFile',
|
|
52
|
+
invocation: { kind: 'direct' },
|
|
53
|
+
parameters: [
|
|
54
|
+
{ name: 'name', wire: 'name', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#FileName', schema: fileNameSchema } },
|
|
55
|
+
],
|
|
56
|
+
result: { mode: 'strict', typeSymbol: 'sage-mem/types#FileContent', schema: readResultSchema },
|
|
57
|
+
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'sage-mem#memory/writeFile',
|
|
61
|
+
service: 'memory',
|
|
62
|
+
namespace: 'memory',
|
|
63
|
+
method: 'writeFile',
|
|
64
|
+
invocation: { kind: 'direct' },
|
|
65
|
+
parameters: [
|
|
66
|
+
{ name: 'name', wire: 'name', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#FileName', schema: fileNameSchema } },
|
|
67
|
+
{ name: 'content', wire: 'content', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#FileContent', schema: fileContentSchema } },
|
|
68
|
+
],
|
|
69
|
+
result: { mode: 'strict', typeSymbol: 'sage-mem/types#WriteResult', schema: writeResultSchema },
|
|
70
|
+
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'sage-mem#memory/deleteFile',
|
|
74
|
+
service: 'memory',
|
|
75
|
+
namespace: 'memory',
|
|
76
|
+
method: 'deleteFile',
|
|
77
|
+
invocation: { kind: 'direct' },
|
|
78
|
+
parameters: [
|
|
79
|
+
{ name: 'name', wire: 'name', source: 'json', codec: { mode: 'strict', typeSymbol: 'sage-mem/types#FileName', schema: fileNameSchema } },
|
|
80
|
+
],
|
|
81
|
+
result: { mode: 'strict', typeSymbol: 'sage-mem/types#DeleteResult', schema: deleteResultSchema },
|
|
82
|
+
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
model: {
|
|
86
|
+
"services": [],
|
|
87
|
+
"events": [],
|
|
88
|
+
"objects": [],
|
|
89
|
+
},
|
|
90
|
+
}
|